Studieplan: Funktioner

Mål: Att lära sig mer om funktioner, t.ex. överlagring av funktioner, underförstådda parametrar (default-parametrar) och rekursiva (självanropande) funktioner. Även något om separatkompilering, dvs att dela upp ett program i flera olika filer.
Läsanvisning: kapitel 4 i "C++ direkt", huvudsakligen kapitlen 6-7 i "Programmeringsteknik med C++".


Studieplan

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen.


Översikt

Alla icketriviala datorprogram vinner på att delas upp i mindre delar. I C++ kan detta innebära att man skriver program som består av många självständiga funktioner och s.k. klasser, som det handlar om mest i TDB2. Varje funktion eller klass bör ha en överblickbar uppgift.

Genom denna uppdelning blir programmen enkla att förstå och att underhålla och enklare att återanvända.

I C++ får flera funktioner ha samma namn, om de har olika antal parametrar eller parametrartyper. Man säger att funktionerna är överlagrade.

I C++ kan en funktion ha fördefinierade värden i parameterlistan. Detta innebär vid funktionsanrop, att alla parametrarna inte behöver vara specificerade. Man säger att funktionen har underförstådda parametarar (default parameters).

I C++ kan en funktion anropa sig själv. Detta kallas för rekursiva anrop och används då problemet i sig kan formuleras rekursivt.

I C++ kan ett program bestå av flera separata delar, dvs de olika separata delarna delas upp i olika programfiler. Vanligtvis delas varje del upp i en definitionsfil och en inkluderingsfil. För att kunna köra programmet gör man en s.k. separatkompilering. En bra anledning till att dela upp kod på flera filer, är att det blir lättare att återanvända redan skriven kod.

Funktioner - Överlagring

Överlagrade funktioner

I C++ är det tillåtet att ha samma namn på två funktioner under förutsättning att parameteruppsättningen skiljer sig åt. Att ha samma funktionsnamn med olika parameteruppsättning kallas av överlagra funktioner (overloaded functions).

Man kan tycka att detta verkar onödigt, men faktum är att koderna blir lättare att förstå om man får döpa funktioner t.ex. till det man vill.

int max(int ..., int ...), double max(double ..., double ...)

Ett exempel på överlagrade funktioner, som ofta visas i C++-läroböcker, är funktioner som beräknar det största värdet av två eller flera element, max:

#include <iostream>

using namespace std;

 

int max( int a, int b )

{

  if ( a > b )

    return a;

  else

    return b;

}

 

double max( double a, double b )

{

  if ( a > b )

    return a;

  else

    return b;

}

 

int max( int a, int b, int c )

{

  return max( a, max(b,c) );

}

 

double max( double a, double b, double c )

{

  return max( a, max(b,c) );

}

 

int max( int arr[], int num )

{

  int m = arr[0];

 

  for ( int i = 0; i < num; i++ )

    if ( m < arr[i] )

      m = arr[i];

  return m;

}

 

double max( double arr[], int num )

{

  double m = arr[0];

 

  for ( int i = 0; i < num; i++ )

    if ( m < arr[i] )

      m = arr[i];

  return m;

}

 

int main()

{

  cout << max(1, 4, 2) << endl;

  cout << max(1.2, 2.3) << endl;

 

  int a[] = { 4, 1, 2, 7, 12};

 

  cout << max(a, 3) << endl;

  cout << max(a, 5) << endl;

 

  double d[] = { 2.3, 7.4, 5.6, 11.2, 10.5};

 

  cout << max(d, 3) << endl;

  cout << max(d, 5) << endl;

 

}

Körningsexempel:

4

2.3

4

12

7.4

11.2

Att tänka på

Observera att funktionerna måste skilja sig åt i parameteruppsättningen, typen på returvärdet är inte åtskiljande. Följande två funktioner kan alltså inte vara deklarerade samtidigt:

int max( int a, int b )

{

  if ( a > b )

    return a;

  else

    return  b;

}

 

double max( int a, int b )

{

  if ( a > b )

    return double(a);

  else

    return double(b);

}

Överlagrade konstruktorer

Ordet överlagrad kommer att återkomma i lektionen "Klasser - intro" här i kursen. Innebörden är densamma, dvs att man vill ha samma namn på flera olika specialfunktioner, som kallas konstruktorer, men att antal parametrar t.ex. varierar.

 

 

Funktioner - Underförstådda parametrar

När man anropar en funktion måste man normalt ange alla parametrar. I C++ finns emellertid också möjligheten att låta en eller flera parametrar vara underförstådda ifall de inte anges vid anropet, s.k. underförstådda parametarar (default parameters).

Ett vanligt exempel i C++-läroböcker är funktionen power för att beräkna ett tal upphöjt till ett annat:

double power( double x, int n = 2 )
{
  double p = x;
 
  for ( int i = 2; i <= n; i++ )
     p *= x;
  return p;
}

Vi har här angivit att den andra parametern skall ges värdet två (2) om den inte anges explicit i anropet från ett huvudprogram:

cout << power(3.5, 2) << endl; // 3.5*3.5
cout << power(3.5)    << endl;   // 3.5*3.5 (2 underförstått)
cout << power(3.5, 4) << endl; // 3.5*3.5*3.5*3.5

Viktigt - sist i listan

Man kan ha flera underförstådda parametrar men de måste komma sist i parameterlistan, dvs mest till höger.

Dessutom kan man bara utelämna parametrar från slutet, dvs om en funktion har två underförstådda parametrar kan man inte ange den sista utan att också ha angett den näst sista.

 

 

Funktioner - Rekursion

En funktion kan anropa sig själv. Sådana funktioner kallas rekursiva och används när det gäller att hitta lösningar till vissa sorters problem, dvs sådana problem som är specificerade på ett rekursivt sätt, t.ex. vissa matematiska problem. Detta kan låta ganska kryptiskt och är lite som trolleri ibland.

Rekursion används en hel del i samband med pekare och s.k. länkade listor, som du kan läsa om i fortsättningskursen till TDB2.

Ett matematiskt exempel

Det vanligaste exemplet man brukar visa då man tar upp rekursion är fakultetsberäkning, som matematiskt skrivs

n!

och som kan beskrivas så här:

n! = 1*2*3*4*...*(n-1)*n

Exempel: 4! = 24, ty 1*2*3*4 = 24.

Ickerekursiv formulering

Vi börjar med att visa en ickerekursiv funktion som beräknar n!:

int fakult0 ( int n )
{
  int prod = 1;
  
  for ( int i = 2; i <= n; i++ )
    prod *= i;
 
  return prod;
}

I funktionen ovan använder vi iteration eller repetition för att bygga upp produkten.

Rekursiv formulering

Problemet kan också beskrivas med rekursiv matematisk formel. De matematiska reglerna för att beräkna n! är följande:

·   Om n <= 1 returnera 1

·   annars returnera n * (n-1)!

Denna beskrivning av problemet leder naturligt till följande funktion:

int fakultet( int n )
{
  if ( n <= 1 )
    return 1;
  else
    return n*fakultet(n-1);
}

Funktionen fakultet anropar sig själv med argumentet "inparametern minus 1". Utan denna nedräkning skulle aldrig stoppvillkoret bli uppfyllt.

Det är mycket viktigt att alla rekursiva funktioner har ett stoppvillkor som avslutar rekursionen - i vårt fall slutar vi när n har blivit lika med 1.

Konstigt - vad händer?

Man kan tycka att rekursion är konstigt men faktum är att rekursiva funktioner fungerar på exakt samma sätt som andra funktioner:
när funktionen anropas beräknas de aktuella parametrarna, varefter minnesutrymme för de formella parametrarna allokeras, och de aktuella parametrerna kopieras till de formella. Nytt minnesutrymme allokeras vid varje anrop. Varje upplaga av funktionen har sin egen uppsättning av de formella parametrarna (n i fallet ovan). Satserna i funktionen utförs och man återvänder till den punkt varifrån funktionen anropades: i fallet ovan till efter return-satsen.

Antag att fakultet anropas med värdet 4. Den formella parametern blir 4 och else-grenen utförs. Vi får ytterligare tre anrop innan villkoret blir uppfyllt:

formell parameter    vad returneras:
      x                x * fakultet(x-1)
      4                4 *    6          = 24
      3                3 *    2          =  6
      2                2 *    1          =  2
      1                1

Observera att det är på "återvägen" som beräkningarna sker: vi "gräver oss" först ner till 1 (vänstra sidan) och "dyker sedan" upp igen till det första anropet (högra sidan).

Idén bakom alla rekursiva funktioner är: hitta ett specialfall som är enkelt att lösa (n <= 1 ovan). Förenkla sedan problemet och hoppas att den funktion man just skriver redan finns och löser det enklare problemet.

Ett ickematematiskt exempel

Vad tror du att följande rekursiva funktion gör?

int doWhat( int a[], int n )
{
  if ( n == 1 )
    return a[0];
  else
    return a[n-1] + doWhat( a, n-1 );
} 

Svar:
Ett annat program, t.ex. huvudprogrammet, skickar arrayen a och arraystorlek n till funktionen doWhat som rekursivt summerar arrayens element och returnerar denna summa.

Exempel: Om n = 1 så sker inga rekursiva anrop, utan funktionen returnerar summan av arrayelementen (dvs bara ett finns!).

Om n = 5 och arrayen har element [3, 5, 1, 2, 8] så sker detta:

n = 5:  return a[4] + doWhat( a, 4 );   anropet sker före "+"
n = 4:    return a[3] + doWhat( a, 3 );   och ett till anrop
n = 3:      return a[2] + doWhat( a, 2 );    och ett till anrop
n = 2:        return a[1] + doWhat( a, 1 );     och ett till anrop
n = 1:          return a[0];                      Nu är det stopp
n = 1:          = return 3                        Återhopp, ta med sig a[0]=3
n = 2:        = return 5+3 (8)                  och summera, returnera upp igen
n = 3:      = return 1+8 (9)                 och summera, returnera upp igen
n = 4:    = return 2+9 (11)               och summera, returnera upp igen
n = 5:  = return 8 + 11 (19)            och summera, returnera upp igen

Funktionen kommer att returnera talet 19 till det anropande programmet.

Exempel

Testa lite rekursion själv! Klicka på länkarna till filerna nedan för att se hela programmet med fakult0, fakultet och doWhat-funktionerna:

 

 

Funktioner - Separatkompilering

Vi lever inte som vi lär i TDB2...

I C++ delar man ofta upp sin kod på flera filer, t.ex. så att det skall vara lätt att dela upp arbetet mellan olika programmerare, lätt att återanvända gammal kod etc.

Du skall känna till att det är så i C++, men i denna kurs kommer vi tvinga dig redovisa inlämningsuppgifter med allt i en enda fil!, för att förenkla för dig som lämnar in, för de studenter och lärare som skall bedöma dina uppgifter. Det är enklare att hantera helt enkelt.

Överkurs

Vill du förenkla livet för dig, så ta det här med flera filer och s.k. separatkompilering som överkurs, så länge du är med på att man ofta arbetar i flera filer!

Uppdelning i filer

När man skriver program (alltså ett antal funktioner eller klasser) skall man skilja på deklaration och definition (= implementation).

För varje funktion (eller klass) skall två olika filer skrivas:

·   en som innehåller deklarationen av funktionen (normalt med suffixet .h)

·   en som innehåller implementationen, dvs innehållet/koden i funktionen. Denna fil kan ha olika suffix, de vanligaste är .cpp, .cc, i Borland C++ används normalt .cpp och det ska vi också försöka hålla oss till under denna kurs.

Deklaration = gränssnitt mot omvärlden

En viktig anledning till att skilja mellan dessa två filer är att andra programmerare som skall använda våra funktioner aldrig skall behöva se annat än deklarationen. Vi kallar deklarationen funktionens gränssnitt mot yttervärlden.

Själva implementationen kan ses som en svart låda: den tar några parametrar, löser ett väldefinierat problem och returnerar svaret. Alla .cpp-filer som använder funktionen inkluderar .h-filen där funktionen är deklarerad.

Ett exempel med min

Filen min.h, deklarerar funktionen min, som skall beräkna och returnera det minsta av två heltal:

int min( int a, int b );

Till denna fil har vi filen min.cpp, som implementerar funktionen:

#include "min.h"
 
int min( int a, int b )
{
if ( a < b )
return a;
else
return b;
}

Och sist ett litet testhuvudprogram:

#include 
#include "min.h"
using namespace std;
 
int main()
{
int tal1, tal2;
cout << "Ge två heltal: " << endl;
cin >> tal1 >> tal2;'
cout << "Minst var talet " << min( tal1, tal2) << endl;
return 0;
}

 

Observera include som behövs i båda .cpp-filerna. Systemets include-filer skall omges med < och > medan de vi skriver själva skall omges med citationstecken, dvs ". För fullständighets skull visar vi också utskriften:

56
4
4
7
12

Kompilering och länkning

Det är viktigt att inse skillnaden mellan att inkludera en funktionsdeklaration och att länka in den kompilerade funktionen.

Antag t.ex. att vi har filerna min.h, min.cpp, testamin.cpp från föregående avsnitt. Vi kan då kompilera min.cpp och testamin.cpp separat - men filen min.h måste inkluderas i båda filerna så att kompilatorn kan kontrollera att vi har rätt antal parametrar. Om man inte inkluderar .h-filen i testamin.cpp får man fel i stil med blabla undeclared.

Vid kompileringen skapas de båda filerna min.o och testamin.o. Dessa båda måste sedan länkas ihop till programmet testamin.

Vi har alltså två separata steg:

·   kompilering

·   länkning

Det är viktigt att inse att det inte räcker att inkludera min.h i testamin.cpp: maskinkoden för själva funktionen måste också finnas tillgänglig för att programmet skall kunna köras.

Schematiskt skulle detta kunna beskrivas så här:

-källkod-                  -objektkod-
---------     KOMPILERING  -----------  LÄNKNING
 
min.cpp       --------->   min.obj      --|
                                          |-----> testamin.exe
testamin.cpp  --------->   testamin.obj --|

där alltså kompilatorn översätter källkoden till objektkoden och länkaren sedan plockar ihop olika objektpusselbitar till en exekverbar (körbar) fil. Om en eller flera pusselbitar saknas får man fel i stil med Unresolved reference: blabla

Många funktioner, de matematiska t.ex. finns i bibliotek: för att använda dem krävs dels att man inkluderar filen (t.ex. math.h), dels att man länkar med det bibliotek som filen ligger i. Detta sker oftast automagiskt i Borland C++.

I praktiken...

Man kan gå till väga på lite olika sätt då man kompilerar flera filer samtidigt. Arbetar man i Borlands grafiska miljö, så skapar man något som heter projekt, men gör man som i TDB1, dvs använder emacs/dos, så blir det så här:

bcc32 testamin.cpp min.cpp 

Sedan kör man som vanligt.

När man arbetar med flera filer, så vill man ibland enbart kompilera en enda C++-fil, bara för att se om man har skrivit den språkligt korrekt enligt C++-språket. Detta görs så här:

bcc32 -c testamin.cpp

vilket kommer att ge en s.k. objektfil som har suffix ".obj", nämligen testamin.obj.

Har man kompilerat alla sina C++-filer separat med bcc32 -c, så kan man sätta ihop dem, länka, med

bcc32 testamin.obj min.obj

 

 

Funktioner - Laboration

Uppgift 1

I programmet nedan saknas C++-kodrader. Skriv klart deklaration och implementation av funktionen summa, som skall summera ett visst antal element i följd i en array.

#include

using namespace std;

 

const int MAX = 10;

 

double summa( ... parameterlista ... );

 

int main()

{

  double a[MAX] = { 1.1, 3.1, 2.1, 5.4, 1.3,

                   17.0, 1.4, 1.2, 1.1, 1.7 };

 

  cout << "Här är alla arrayelement:" << endl;

 

  for ( int i = 0; i < MAX; i++ )

    cout << a[i] << " ";

 

  cout << endl;

 

  cout << "Summa av alla arrayelement: "

       << summa( a, MAX ) << endl;

  cout << "Summa från index = 2 t.o.m 4: "

       << summa( a, MAX, 2, 4 ) << endl;

  return 0;

}

 

double summa( ... parameterlista ... )

{

// Summera element i en array.

.....

 

}

Här kan du se ett körexempel:

Här är alla arrayelement:

1.1 3.1 2.1 5.4 1.3 17 1.4 1.2 1.1 1.7

Summa av alla arrayelement: 35.4

Summa från index = 2 t.o.m 4: 8.8

 

Uppgift 2

Nedan ser du ett C++-program som skriver "Hurra!" ett antal gånger.

int main()

{

  int antal;

  cout << "Hej! Hur många gånger skall vi hurra?" << endl;

  cin >> antal;

 

  hurra( antal );

 

  return 0;

}

Körningsexempel visar detta:

Hej! Hur många gånger skall vi hurra?

5

Hurra!

Hurra!

Hurra!

Hurra!

Hurra!

Skriv olika varianter av funktionen hurra.

a.      Kopiera det ofullständiga programmet, hurra0.cpp och döp om det till hurra_a.cpp. Skriv till C++-kodrader som saknas i funktionen hurra. Du skall använda en iterativ (upprepande) algoritm. Kompilera och testkör.

b.      Kopiera det ofullständiga programmet, hurra0.cpp och döp om det till hurra_b.cpp. Skriv till C++-kodrader som saknas i funktionen hurra. Du skall använda en rekursiv algoritm. Kompilera och testkör.

Lösningsförslag

1.      Uppgift 1:
array1.cpp

2.      Uppgift 2:

1.      hurra_iter.cpp

2.      hurra_rek.cpp eller hurra_rek_annan.cpp

 

 

Studieplan: Klasser - introduktion

Mål: Att komma igång med objektorienterad programmering (OOP) genom att införa klasser och objekt.
Läsanvisning: kapitel 7 i "C++ direkt", kapitel 9 i "Programmeringsteknik med C++".


Studieplan

1.      Läs igenom materialet på kommande webbsidor + tillhörande programexempel, som kan laddas hem som C++-filer för egna testningar.

2.      Läs i kompendiet. Vid behov, ladda hem filer och testa själv.

3.      Utför laborationen.

4.      Gör första inlämningsuppgiften om klasser.

5.      Invänta lärarens kommentarer på inlämningsuppgiften.

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen.


Översikt

Olika sorters programmeringsspråk

Den del av C++ som ingår i kursen Datorer och programmering TDB1 bygger på samma idéer som rent procedurella språk, t.ex. Pascal, C och FORTRAN. I procedurella språk skiljer man på algoritm och data. Ett program består av ett antal funktioner samt ett huvudprogram som anropar dessa. En funktion kan ses som en svart låda som man stoppar in ev. indata i och som man får ut ev. utdata från (via return-satsen). När programkörningen befinner sig i en funktion, så styr algoritmen i denna hur utdata skall beräknas genom att använda indata.
(Anmärkning: Alla funktioner behöver ej indata och/eller utdata, t.ex. void-funktioner har inget utdatavärde).

En ny infallsvinkel på problemlösning och programmering är att se data och algoritm som en enda enhet, ett s.k. objekt. I objektorienterad programmering (OOP) delar man in sitt problem i olika objekt som kommunicerar med varandra på något vis.

Alla objekt tillhör någon klass, som fungerar som en slags mall för hur objekten skall se ut. I och med att vi börjar med klasser i C++ här i Datorer och programmering TDB2, så börjar vi med objektorienterad programmering.

Tillsammans med JAVA är C++ ett av de mest använda programmeringsspråken just nu.

Exempel

Skall man simulera en affär, så har man kanske ett objekt Nisses_Livs som representerar en hel affär. I detta objekt har man kanske en array av objekt av typen Vara, som egentligen innehåller både data (pris, antal i lager, varunamn) och algoritmer (speciella funktioner: Saelja, LaesIn, ProcentPris).

Fördelar

En av fördelarna med OOP är att det blir lättare att skapa stora program, eftersom man delar in dem i mindre mer hanterbara delar, objekt. Det kan kännas mer naturligt att dela in "världen" i objekt (personer, banker, bankkonton, ...) än bara som in- och utdata till funktioner.

 

 

Klasser - introduktion

Olika sorters data

Låt oss studera introducerandet av klasser och objekt ur ett datatypsperspektiv.

·   Enkla datatyper kan deklareras så här:
double tal; int datum;
Tal har följande operationer: addition(+), subtraktion(-), division(/), heltalsdivision(%), multiplikation(*).
En variabel av enkel datatyp kan anta ett enda värde, t.ex.:
tal = 0.5*datum;

·   Arrayer kan deklareras så här:
double x[max];
En array är en samling data av samma datatyp. En operation är t.ex. indexering: med
index = 3 kan vi komma åt det fjärde elementets värde i arrayen x[3] = 34.66;
Kom ihåg att arrayindex alltid börjar på 0 (noll).

·   En klass är en gruppering av element med lika eller olika sorters datatyper, t.ex. enkla, arrayer eller andra klasser. Programmeraren bestämmer själv klassens operationer genom att skriva speciella funktioner för klassen.

Egentligen har du redan sett och använt en klass, nämligen string-klassen, som är inbyggd i C++ för att förenkla hanteringen av strängar för programmeraren. Den har egna funktioner och operationer, t.ex.:

string rad;
cout << "Ge en textrad: " << endl;
getline( cin, rad );
cout << " Antal tecken i raden är: "
     << rad.size() << endl;

Här är rad ett objekt i klassen string. C++-koden rad.size() innebär att antal tecken i strängen rad beräknas och returneras.

Hur tänker man objektorienterat?

Exempel med vara

Antag vi skall lösa följande problem:

Skriv ett C++-program för hantering av varor i en affär med allt vad det innebär: att sälja till kunder, att inköpa från grossist, att sätta nytt pris, att skriva ut information om varje vara, etc. Affären öppnas varje morgon och stängs varje kväll.... etc.

När man tänker objektorienterat så ser man alla substantiv i problemet som eventuella klasser och alla verb som eventuella funktioner. Här skulle det innebära följande klasser:

·   Vara som har funktionerna sälja, köpa in, sätta pris, skriva ut.

·   affär som innehåller många varor och har funktionerna öppna, stäng

Vi arbetar nu vidare med klassen Vara enbart. När man arbetar med OOP och funderar på hur en klass skall se ut mer i detalj, så skall man alltid ställa sig två frågor:

1.      Hur skall varan representeras? Dvs vilka egenskaper har den?

2.      Vad vill vi kunna göra med varan? Dvs vilka operationer vill man ha?

Svar på 1: Hur skall varan representeras?

En vara kan t.ex. karaktäriseras av

Namn              Pris            Antal (i lager)
 
(string)     (double)   (int)

De ingående delarna i klassen, dvs Namn, Pris, Antal kallas för attribut eller datamedlemmar.

I sitt huvudprogram skall man sedan "tillverka varor", dvs deklarera några variabler att ha datatypen Vara. Dessa variabler är då objekt i klassen Vara.

Svar på 2: Vad vill vi kunna göra med varan?

Vi vill kunna göra följande med en vara, dvs ett Vara-objekt:

·   Ge varan initialvärden, t.ex. läsa in data om en ny vara.

·   Få hem nya varor av samma sort, dvs addera varor till lagret.

·   Sälja varor, dvs ta bort varor ur lagret.

·   Sätta nytt pris på varan, givet en procentfaktor.

·   Skriva ut information om en vara.

Operationer på ett objekt definieras som s.k. medlemsfunktioner eller metoder i klassen. Punkterna ovan kommer att representeras av medlemsfunktionerna: LaesIn, TillLager, Saelja, ProcentPris, SkrivUt

 

 

Klasser - kod i C++

Klassdefinition i C++

Generellt ser en klassdefinition ut så här i C++:

class Klassnamn
{
private:
  deklarationer av gömda attribut
  och ev. gömda medlemsfunktioner 
 
public:
  deklarationer av synliga medlemsfunktioner
};

I fallet med klassen Vara kan det se ut så här (där vi utelämnat koden i metoderna, {...}):

class Vara
{
private:
  string Namn;
  double Pris;
  int    Antal;
 
public:
 
  void LaesIn()                      //---Läs info om vara.
  {...}
  void TillLager( int AntalNya )     //---Nya till lagret.
  {...}
  void Saelja( int AntalSaelj )      //---Sälj vara. 
  {...}
  void ProcentPris( double Faktor )  //---Sätt nytt pris.
  {...}
  void SkrivUt()                     //---Skriv info om vara.
  {...}    
};

Observera att det är semikolon (;) efter sista }-klammern!

Användandet av metoder i en klass

Hur "anropar" ett objekt en metod i sin egen klass?

När ett objekt skapat i main vill använda en av sina metoder så används s.k. punktnotation:

objektnamn.metodnamn(ev. parametrar)

Exempel:

int main()
{
  Vara enVara;        // Skapar objektet enVara. 
  enVara.LaesIn();    // Läs in data till enVara-objektet.
  ...

Här är LaesIn en void-metod (dvs den returnerar inget värde) och anropas då direkt på en rad bara (jfr vanliga void-funktioner).

Vad menas med "private" och "public"?

·   Man låter attributen vara privata (private). Detta innebär att ingen utanför klassen (t.ex. huvudprogrammet) kan komma åt dessa attribut. Enbart klassens egna funktioner får använda dessa.

·   Alla data kan kommas åt direkt från alla medlemsfunktioner utan parameteröverföring.

·   Man låter medlemsfunktioner vara publika (public). Detta innebär att de kan kommas åt utifrån, t.ex. anropas från huvudprogrammet.

Klassen Vara med tillhörande huvudprogram

Ibland när man programmerar så kan det vara lättare att börja med main först. Objekt- och metodnamn bör vara lätta att förstå. Exempel:

// Huvudprogram som testar klassen Vara.
int main()
{
  Vara enVara;        // Skapar objektet enVara. 
  enVara.LaesIn();    // Läs in data till enVara-objektet.
  enVara.SkrivUt();   // Skriv ut info om varan.
 
  enVara.ProcentPris( 1.10 ); // Prisökning 10%.
  enVara.TillLager( 100 );    // 100 nya från grossisten.
  enVara.Saelja( 50 );        // Sälj 50 st.
  enVara.SkrivUt();   //Skriv ut info om varan.
 
  return 0;
}

Körningsexempel (fet stil är det användaren skriver vid tangentbordet):

Ge varans namn:
Mjölk
Ge pris och antal i lager:
7.00 125
Varunamn: Mjölk    Pris: 7   Antal i lager: 125
Varunamn: Mjölk    Pris: 7.7 Antal i lager: 175

Kommentarer till huvudprogrammet:

·   I huvudprogrammet, main, deklareras ett objekt enVara hörande till klassen Vara.

·   Med hjälp av medlemsfunktioner kan vi bearbeta attributen (data) i klassen. Från början har objektets attribut odefinierade värden. För att ge objektet värden används här metoden metoden LaesIn, som ger dess attribut värden genom inläsning från tangentbord.
Observera att attributen automatiskt följer med till metoden och får nya värden, utan att de är med i parameterlistan!

·   Programmet skriver sedan ut information om objektet (metoden SkrivUt, ändrar lite på Pris och Antal (metoderna ProcentPris, TillLager, Saelja), samt skriver ut objektets värden igen.

Exempel
Studera nu hela programfilen (klicka på länken):
Vara1.cpp (programfil)

Klasser - kodstruktur

Snyggare och mer lättläst kod

För att få ett välstrukturerat och överblickbart program så bör man ej ha koden för metoderna i själva klassdefinitionen. Jämför med hur man kan deklarera vanliga funktioner överst i en fil och sedan definiera dem med kodinnehåll längre ned i filen under huvudprogrammet.

Senare kommer vi att dela upp dessa programdelar i olika filer, dvs klassdefinition i en fil, metodimplementationen i en och huvudprogrammet i en tredje fil.

Vi rekommenderar följande ordning i filen:

1.      Först klassdefinition, som visar attributen och deras typer samt metoderna och deras egenskaper (eventuella parametrar, returtyp, namn).

2.    class Vara

3.    {

4.    private:

5.      string Namn;

6.      double Pris;

7.      int    Antal;

8.     

9.    public:

10.  void LaesIn();

11.  void TillLager( int AntalNya );

12.  void Saelja( int AntalSaelj );

13.  void ProcentPris( double Faktor );

14.  void SkrivUt();                  

15.};

Observera semikolon (;) efter varje metoddeklaration!

16.  Sedan huvudprogrammet, dvs main. Programmeraren av huvudprogrammet behöver ej se innehållet i klassens metoder, enbart deras "interface", vilket ges i deklarationen.

17.int main()

18.{

19.  Vara enVara;       

20.  enVara.SkrivUt();  

21.  enVara.LaesIn();   

22....

Notera att punktnotation används vid metodanropen.

23.  Sist i filen kommer själva innehållet i metoderna, dvs koden. Detta kallas för definition av metoder eller implementation av metoder.

24.//=========================================

25.// Implementation (definition) av metoder i

26.// klassen Vara.

27. 

28.void Vara::LaesIn()

29.{

30.  cout << "Ge varans namn:" << endl;

31.  getline( cin, Namn );

32.  cout << "Ge pris och antal i lager:"

33.       << endl;

34.  cin >> Pris >> Antal;

35.  cin.get(); // Kan läsa in ny vara senare...

36.}

37.//----------------------------------------

38.void Vara::TillLager( int AntalNya )

39.{

40.  Antal += AntalNya;

41.}

42....

Observera att texten Vara:: i koden anger klasstillhörighet, dvs att just dessa metoder hör till klassen Vara.

I stora program kan man ha flera klasser i omlopp och det kan vara svårt för kompilatorn att fatta "vem är vem?", då olika klasser har samma metodnamn. T.ex. är det vanligt med namn som LaesIn, SkrivUt....

Exempel
Klicka på länken till filen
Vara2.cpp för att se hur en snyggare programfil ser ut.
Även körningsresultatet visas.

 

 

Klasser - specialmetoder

Konstruktorer, selektorer och destruktorer

Det finns speciella sorters metoder i C++:

·   Konstruktorer: skapar objekt och initierar samtidigt dess attributvärden

·   Selektorer: returnerar objekts attributvärden

·   Destruktorer: dödar objekt

Egentligen finns alltid en inbyggd, osynlig konstruktor som inte gör någonting annat än konstruerar objekt, dvs attributen är fortfarande odefinierade. Denna anropas då objekt skapas. Det finns också en inbyggd, osynlig destruktor, som dödar objekt då de ej längre behövs, t.ex. vid en slutklammer i ett programblock, }. Destruktorer anropas aldrig explicit. Exempel:

int main()
{
  Vara leksaksBil; // En s.k. konstruktor anropas
                   // automatiskt. Objektet skapas.
  ...
                 // Här dödas objektet automatiskt
                 // utan att vi gör något.
}

 

Konstruktorer

När man deklarerar vanliga variabler (enkla datatyper och arrayer) så kan man om man vill initiera dem samtidigt, t.ex.

int sum = 0;
double A[3] = { 1.0, 3.7, -5.24 };

 

Objekt initieras med hjälp av konstruktorer. Dessa är speciella metoder med samma namn som klassen själv. Två varianter finns:

1.      Parameterlös konstruktor kallas standardkonstruktor eller default-konstruktor. Dessa anropas automatiskt vid deklaration av objekt, t.ex.

2.    int main()
3.    {
4.      Vara Mjoelk; // Här anropas standard-
5.                   // konstruktorn automatiskt!
6.      ...
7.    }

Definitionen av standardkonstruktorn kan se ut så här:

      
Vara::Vara()
{
  Namn = "";   // Tilldela så tom sträng
  Pris = 0.0;  // Sätt pris till 0.0 kr
  Antal = 0;   // Antal i lager = 0 st
}

 

8.      Konstruktor med en eller flera parametrar, kallas även extrakonstruktor eller överlagrad konstruktor (eftersom den heter likadant som standardkonstruktorn), dvs en konstruktor med en eller flera parametrar. Man kan ha flera extrakonstruktorer (om alla dessa har olika antal parametrar). Exempel:

9.    int main()
10.{
11.                 // Deklarera och initiera:
12.  Vara Mjoelk( "Mjölk", 6.75, 15000 ); 
13.                 // Standardkonstruktor:
14.  Vara Franska;  
15. 
16.  // Eller använd extrakonstruktor så här:
17.  Franska = Vara( "Småfranska", 1.50, 45000 ); 
18.  ...
19.}

Ovan visas två sätt att använda en överlagrad konstruktor. I den senare, så bygger vi upp ett objekt i högerledet med hjälp av konstruktorn och tilldelar detta värde till objektet Franska.

Det är helt OK att tilldela hela objekt i C++! Egentligen kopieras varje attribut för sig, men det sker bakom kulisserna automatiskt.

I definitionen kan det se ut så här:

       
Vara::Vara( string inNamn,
         double inPris, int inAntal )
{
  // Initiera attributen mha värden
  // som kommer via parameterlistan     
 
  Namn = inNamn;      
  Pris = inPris;
  Antal = inAntal;
}

 

Selektorer

Selektorer (getters) används för att få ut attributvärden ur ett objekt. Eftersom attributen är "private"-deklarerade, så hålls de gömda inne i objektet. Enbart metoder i samma klass kan komma åt dem.

Exempel: Antag att vi har en metod HaemtaAntal som returnerar värdet av attributet Antal, då kan användandet av metoden se ut så här:

int main()
{
  Vara Mjoelk;
  Mjoelk.LaesIn();
  cout << "Antal i lager just nu: " 
       << Mjoelk.HaemtaAntal() << endl;
  ...
}

 

Definition av selektorer kan se ut så här:

//--------------------------------------
double Vara::HaemtaPris()
{
  return Pris;
}
//--------------------------------------
int Vara::HaemtaAntal()
{
  return Antal;
}
//--------------------------------------
string Vara::HaemtaNamn()
{
  return Namn;
}
//--------------------------------------

 

Destruktorer

Destruktorn i en klass har som enda uppgift att döda objekt. Programmeraren anropar aldrig denna! Destruktorn anropas alltid automatiskt då det är dags att döda ett objekt, vilket t.ex. sker före en slutklammer av typen }. Antingen kan det vara slutklammern i hela programmet eller i ett loop- eller villkorsblock där objekt har skapats.

När man arbetar med dynamiskt allokerat minnesutrymme (se avsnittet om pekare i denna kurs), så är det viktigt att frisläppa minnesutrymme som inte längre behövs. Om en klass har "dynamiska attribut" (skapade med new i konstruktorerna), så bör man ha en destruktor som gör delete.

Program utan pekare behöver ingen destruktor.

Destruktorn, som alltid är parameterlös, har samma namn som klassen, men med ett tilde-tecken (~) framför:

Vara::~Vara()
{
 cout << "Hej! Nu dödas jag";
 SkrivUt();
}

Ovan har vi en liten ledtext i destruktorn för att visa när den anropas av programmet. I exemplet ser vi också hur man kan anropa en metod i samma klass direkt utan punktnotation.

Mer om användandet av attribut och metoder

Begrepp

Vid metodanrop finns alltid ett aktivt objekt, dvs den som använder/anropar metoden. Väl inne i metoden kallar vi detta objekt för aktuella objektet.

När vi säger "inuti klassen" så menar vi inne i någon metod som hör till klassen. När vi säger "utanför klassen" så menar vi t.ex. i huvudprogrammet, i en vanlig funktion eller i en metod i annan klass.

Ett lokalt objekt är ett objekt deklarerat inne i en metod. T.ex. kan det vara ett objekt i parameterlistan till metoden.

Metoder

Som vi nämnt anropas metoder för ett visst objekt med hjälp av punktnotation:

objektnamn.metodnamn( ev. parametrar );

Exempel:

Vara ost;
ost.LaesIn();

Men inuti klassen kan man utelämna objektnamnet och punktnotationen när man menar det aktuella objektet:

metodnamn( ev. parametrar);

Exempel:

void Vara::ProcentPris( double Faktor )
{
  // För testning, skriv info om aktuella objektet:
  SkrivUt();      
  Pris *= Faktor;
}

D.v.s. vi menar att om vi anropar:

ost.ProcentPris(0.98);

så menar vi att den första raden i metodkroppen ska tolkas som ost.SkrivUt(). Det aktuella objektet är underförstått inuti klassen.

Attribut

Attributen är "private-deklarerade", vilket innebär att enbart metoder i samma klass kan komma åt dem och ändra värde på dem. Aldrig någonsin kan någon utanför klassen ändra dem. Det finns två varianter:

·   Aktuellt objekt:
metoden kan komma åt attributen i aktuella objektet direkt utan punktnotation.

·   Lokalt objekt:
metoden kan komma åt attributen till objektet genom punktnotation.

Exempel: Antag vi har en ny metod som heter KollaSammaPris som undersöker om aktuella objektet har samma pris som det objekt som kommer i parameterlistan.

void Vara::KollaSammaPris( Vara annan )
 
// Här har vi att "Pris" hör till aktuella objektet 
// och "annan.Pris" hör till lokalt objekt.
{
  if ( annan.Pris == Pris )
    cout << "samma pris!" << endl;
}

Visserligen skall man inte jämföra två flyttal med "==", men det struntar vi i just här.

Selektorer skickar ut attributvärden till världen utanför klassen.

 

 

Klasser - större exempel

Ett större exempel med konstruktorer, selektorer och destruktor

Låt klassdefinitionen nu se ut så här:

class Vara

{

private:

  string Namn;

  double Pris;

  int    Antal;

 

public:

  // Konstruktorer och destruktor:

  Vara();                                           

  Vara( string inNamn, double inPris, int inAntal );

  ~Vara();                                          

 

  // Selektorer:

  double HaemtaPris();         

  int HaemtaAntal();

  string HaemtaNamn();

 

  // Övriga metoder:

  void LaesIn();

  void TillLager( int AntalNya );

  void Saelja( int AntalSaelj ); 

  void ProcentPris( double Faktor );

  void SkrivUt();                   

};

Med följande huvudprogram:

int main()

{

  // Testa konstruktorerna:

 

  cout << "Nu testar vi konstruktorer..." << endl;

  Vara Boll;                               //(1)

  Vara Ost( "Brie", 145.00, 123 );         //(2)

 

  Vara Franska;                            //(3)

  Franska = Vara( "Franska", 1.50, 12000 );//(4)

 

  // Testa selektorerna:

                                           //(5)

  cout << "Nu testar vi selektorer..." << endl 

       << Franska.HaemtaNamn() << " har priset "

       << Franska.HaemtaPris()

       << " och finns "

       << Franska.HaemtaAntal() << " st i lager!"

       << endl;

                                           //(6)

  cout << "Nu testar vi destruktorn..." << endl;

  return 0;

}

För att visa hur och när konstruktorer och destruktor arbetar, så har vi utskrifter i dessa.

Kommentar till körningen:

·   (1): En vara, Boll deklareras med hjälp av standardkonstruktorn.

·   (2): En vara, Ost deklareras med hjälp av överlagrade konstruktorn.

·   (3): En vara, Franska deklareras med hjälp av standardkonstruktorn.

·   (4): Överlagrade konstruktorn används för att i högerledet skapa ett nytt objekt men namnet "Franska" bl.a. Detta tilldelas vänsterledet, som är ett objekt, Franska. Det är OK med tilldelning av objekt i C++!

·   (5): Kontroll att man kan plocka ut värden från privat-deklarerade attribut med hjälp av metoder.

·   (6): I slutet av varje block där objekt har deklarerats, så dödas de i bakvänd ordning mot hur de skapades.

Exempel

Klicka på länken till filen nedan för att se ett mer utvecklat program för Vara-klassen. Även ett körningsexempel ges.

 

 

Klasser - arrayer

Arrayer av objekt i "main"

För att få ett program som mer efterliknar verkligheten, så deklarerar vi en array av Vara-objekt i huvudprogrammet och kallar denna Artiklar. Vi har kvar testutskrifter i konstruktorer och destruktor för att se när nya objekt egentligen skapas och dör. Allt är precis som i Vara3.cpp, men huvudprogrammet ser nu ut så här:

int main()

{

  // Skapa nu en hel array av objekt i klassen Vara.

  // Antag max 5 st varor nu.

 

  const int MaxVaror = 5;

  int AntalVaror;

 

  Vara Artiklar[MaxVaror];                   //(1)

 

  Artiklar[0] = Vara( "Mjölk", 6.50, 3000 ); //(2)

  Artiklar[1] = Vara( "Lime", 1.00, 150 );   //(3)

  Artiklar[2] = Vara( "Äpple", 2.00, 7000 ); //(4)

  Artiklar[3].LaesIn();                      //(5)

 

  AntalVaror = 4;

 

  cout << "Nu har vi följande varor i lager: " << endl;

 

  for ( int i = 0; i < AntalVaror; i++ )    //(6)

    {

      Artiklar[i].SkrivUt();

    }

 

  cout << "Vi skall sätta om priset på alla varor"

       << ", ge faktorn: ";

  double Fakt;

  cin >> Fakt;

 

  for ( int i = 0; i < AntalVaror; i++ )

    {

      Artiklar[i].ProcentPris( Fakt );

    }

 

  cout << "Skriver ut varor med nya priser: " << endl;

 

  for ( int i = 0; i < AntalVaror; i++ )

    {

      Artiklar[i].SkrivUt();

    }

  return 0;

}

 

Klicka på körningsexempel och hela programkoden nedan, så du har dessa i två olika fönster. Kommentarer till körningen:

·   (1): Här anropas automatiskt standardkonstruktorn MaxVaror antal gånger, dvs fem i vårt fall. Vi ser i körningen att fem tomma objekt skapats. Alla dessa objekt lever naturligtvis kvar.

·   (2)-(4): I högerledet skapas ett nytt objekt med överlagrade konstruktorn, sedan tilldelas element på plats noll i arrayen detta värde. Objekten i högerledet lever fortfarande kvar, dvs nu har vi åtta levande objekt.

·   (5): Artiklar[3] är ett objekt i Vara-klassen, alltså kan metoder i klassen användas. Här sker inläsning till attributen i element på plats tre i arrayen.

·   (6): För att göra samma sak med alla objekt i "affären", t.ex. skriva ut dem, så använder man en loop. Artiklar[i] är ett objekt och alltså kan metoder i klassen användas här.

·   På slutet i körningen ser man att objekten dödas i bakvänd ordning då programmet tar slut.

Exempel

Klicka på länken till filen nedan för att se ett mer utvecklat program för Vara-klassen. Även körningsexempel kan fås upp i eget fönster.

Arrayer som attribut

Vi har sett attribut av typen double, int, string. Attribut kan ha andra datatyper, t.ex. vara arrayer. Exempel:

const int MAX = 100;

 

class datavaerden

{

private:

  int x[MAX], int y[MAX];

  int antal;

 

public:

  datavaerden();

  .....

};

 

 

 

 

 

 

Klasser - uppdelning i flera filer

Datorprogram delas normalt upp i flera filer, s.k. källkodsfiler. Detta kan göras i program som består av en eller flera funktioner eller i program som består av en eller flera klasser.

Det finns olika skäl för detta:

·   Källkodsfilerna blir återanvändbara, flera filer kan använda samma funktion eller klass.

·   Källkodsfilerna blir mindre och lättare att hantera.

·   Med hjälp av include-filer kan man skilja på gränssnitt (= funktionsdeklaration - det som den som använder en funktion behöver veta, dvs hur den anropas) och implementation (själva koden, algoritmen).

Exempel med Vara-klassen

Exempel
Om vi delar upp filen Vara2.cpp i flera filer, så blir det så här:

  1. Vara.h
    innehåller klassdefinition, kallas
    header-fil eller h-fil.
  2. Vara.cpp
    innehåller implementation av metoder.
  3. testaVara.cpp
    innehåller själva huvudprogrammet.

Kommentarer till filerna:

·   testaVara.cpp och Vara.cpp:
Båda dessa filer innehåller

·         #include "Vara.h"

     

vilket innebär att header-filen Vara.h tas med vid kompilering av C++-filerna.

·   Vara.h:
Denna fil innehåller lite kryptisk kod, nämligen:

·         #ifndef __Vara__

·         #define __Vara__

·          

·         ...

·          

·         class Vara

·         {

·         ...     

·         };

·          

·         #endif

där man med brädgård (#) anger diverse direktiv till kompilatorn. Vi går ej in på detaljerna här, men vill dock berätta att om man utelämnar dessa styrkoder till kompilatorn, så kommer kompilatorn att klaga att man försöker deklarera sin klass Vara två gånger, vilket är förbjudet. En och endast en gång per program får man deklarera en klass.

En enkel regel är att alltid ha med dessa rader i sina header-filer, så kan klasserna användas i flera sammanhang utan problem. Man kan döpa "kompilatorvariabeln" till vad som helst, men ofta tar man klassens namn och har en eller två understrykningstecken runt omkring.

Kompilering

Man kan gå till väga på lite olika sätt då man kompilerar flera filer samtidigt. Arbetar man i Borlands grafiska miljö, så skapar man något som heter projekt, men gör man som i TDB1, dvs använder emacs/dos, så blir det så här:

bcc32 testaVara.cpp Vara.cpp

Sedan kör man som vanligt.

När man arbetar med flera filer, så vill man ibland enbart kompilera en enda C++-fil, bara för att se om man har skrivit den språkligt korrekt enligt C++-språket. Detta görs så här:

bcc32 -c testaVara.cpp

vilket kommer att ge en s.k. objektfil som har suffix ".obj", nämligen testaVara.obj.

Har man kompilerat alla sina C++-filer separat med bcc32 -c, så kan man sätta ihop dem, länka, med

bcc32 testaVara.obj Vara.obj

 

I kursen Datorer och programmering TDB2

Du skall känna till att man kan dela upp filer och varför det är bra, men vi rekommenderar att du i inlämningsuppgifterna i TDB2-kursen har all kod i samma fil, eftersom det förenklar för oss alla! (T.ex. om du vill ladda hem och testköra kurskompisars program eller de vill testa dina).

 

 

Klasser - ett cirkelexempel

Cirkel-klassen

Skriv ett program som simulerar ett bollhav på en hamburgerrestaurang (ett sådant som är fullt av små färgglada plastbollar och barn). Bollarna krockar med varandra och med barnen och studsar ibland till och med ut ur inhägnaden.

Här kan det behövas många klasser, t.ex. barn, bollhav, boll. Eftersom vi är lata så simulerar vi i två dimensioner och passar på att döpa bollklassen till cirkel så att den lätt kan återanvändas i andra koder. En klass för själva bollhavet kommer senare i Klasser-Aggregat.

Vilka egenskaper har en cirkel?

Egenskaper = klassens attribut.

Här är det naturligt att svara position, dvs (x,y) och radie. Vill man kan man kanske ha färg, lukt och andra egenskaper också, men det struntar vi i nu.

Vi kommer alltså att ha tre attribut - alla av flyttalsstyp:

x, y, radie.

Vilka operationer vill man ha på en cirkel?

Operationer = klassens metoder.

Vad vill man kunna göra med cirklar?

Svar:
Man vill naturligtvis kunna skapa cirklar, läsa in data till dem, skriva ut information om dem. I bollhavsexemplet krävs att cirklar skall kunna flyttas och att man kan kolla om två krockar, så de studsar bort från varandra. Tänker man att man vill återanvända klassen cirkel senare, t.ex. i ett grafikprogram inom matematisk geometri, så kanske man vill kunna förstora och förminska cirklar, beräkna yta och omkrets. Om tillämpningen är regnprogram med cirkulära vattendroppar, så kanske man vill smälta ihop två droppar till en stor.

Följande metoder bör vi alltså ha:

·   Konstruktorer, en standard och några överlagrade.

·   Selektorer. Vi startar med en, R som returnerar radie.

·   Andra metoder:
lasin, skrivut, flytta, skala, omkrets, yta, krock, ihop

Metoderna mer i detalj

Det är bra om metoderna har namn som beskriver vad de gör. Egentligen borde metodnamn vara verb, men det är inte alltid så lätt att uppnå. Man vill undvika alltför långa namn också.

Beskrivning av metoderna:

·   Konstruktorer:
En standardkonstruktor (alltid parameterlös),

·         cirkel();
      

som får skapa enhetscirklar, dvs radie=1 och mittpunkt i origo.
Två överlagrade konstruktorer

cirkel( double r );
cirkel( double xp, double yp, double r );      
      

den första skapar en cirkel i origo med radie=r, den andra skapar en cirkel i (xp,yp) med radie=r.

·   Inläsning och utskrift:
Inläsning av attributvärden och utskrift av dessa sker med

·         lasin();
·         skrivut();
      

·   Selektorer:
Vi anar att vi ej behöver selektorer, men skriver ändå en:

·         double R();
      

som returnerar radiens värde.

·   Övriga metoder:
Typiska cirkelmetoder:

·         double omkrets();
·         double yta();

returnerar omkrets respektive yta till aktuella objektet.

Dessutom har vi:

void flytta(double dx, double dy);    
      

som flyttar mittpunkten till (x+dx, y+dy).

void skala(double faktor); 
      

som förminskar aktuella objektet om parametern är mindre än 1 och förstorar den om den är större än 1.

Så våra specialare, som är "pricken över i:et" i detta exempel, eftersom de är lite klurigare:

int krock( cirkel annan );
cirkel ihop( cirkel & annan );         
      

Metoden krock kollar om aktuella objektet krockar med cirkeln som kommer som inparameter. Geometriskt innebär det att avståndet mellan cirklarnas mittpunkter är mindre än eller lika med summan av cirklarnas radier. Rita en figur och kolla!

Metoden ihop skall skapa och returnera en ny cirkel genom att smälta ihop aktuella objektet med cirkeln i parameterlistan. Båda dessa får radie=0, vilket betyder att de försvunnit. Den nya cirkelns attribut får i denna enkla algoritm (som säkert ej är fysikaliskt korrekt!) medelvärdet av de två ursprungscirklarnas attribut. Observera att parametern måste vara referensparameter, eftersom dess radie sätts till noll!

Hela koden

Vi skriver programfilen strukturerad med klassdefinition, huvudprogram och metodimplementation (definition av metoderna).

De flesta metoderna är små och förhoppningsvis lättförståeliga, så vi nämner ej alla metoder här. Kommentarer till programmet:

·   Det går utmärkt att returnera värdet av aritmetiska uttryck; t.ex. implementationen av krock och ihop så används lokalt deklarerade objekt, dvs de deklareras antingen i parameterlistan till metoden eller inne i metoden.
Man kan komma åt attibuten till ett lokalt deklarerat objekt om man är i en metod som hör till samma klass som objektet! Punktnotation används.

·         if ( sqrt( (x - annan.x)*(x - annan.x) +
·                    (y - annan.y)*(y - annan.y) ) <=
·                      radie + annan.radie )
·               

Här använder vi alltså annan.x, annan.y, annan.radie.

·   När man skall returnera objekt, så kan det vara lämpligt att skapa ett temporärt objekt som man sätter attributen till, t.ex.

·         cirkel c;  
·         c.x = 0.5*(x + annan.x);
·         c.y = 0.5*(y + annan.y);
·         c.radie = 0.5*(radie + annan.radie);
·         ...
·         return cirkel;
      

Exempel

Klicka på länken till filen nedan för att se ett program för cirkel-klassen. Även körningsexempel kan fås upp i eget fönster.

 

 

Klasser - laboration

Uppgift 1

Skriv ett program för temperaturomvandling. Skriv allt i samma fil, men ha strukturen: klassdefinition, huvudprogram, metodimplementation. Lösningsförslag.

Skapa en klass temperatur enligt följande

class  temperatur

{

private:

  double grader;        // i Celsius.

 

public:

  temperatur();         // default-konstruktor

  temperatur(double t); // överlagrad konstruktor

  void lasin();         // inläsning av grader

  void skriv();         // utskrift av grader

  double haemta_grader();        // returnerar grader

  double skillnad(temperatur t); // skillnaden mellan 2

  double fahrenheit();           // return. i Fahrenheit

  void rapport();                // skriver "rapport"

};

 

Skriv ovan nämnda metoder i klassen temperatur.

Sambandet mellan Celsius-grader (c) och Fahrenheit-grader (f) anges av:

f = 32 + 1.8*c

Metoden rapport skall skriva ut ett av följande uttryck:

Oh! Vad varmt det är idag!

Oh! Vad kallt det är idag!

Det är lagom varmt idag.

beroende på om temperaturen är större än 15 grader, mindre än 5 grader, eller mellan 5 och 15 grader.

Skriv ett huvudprogram som använder metoderna i klassen ovan.

Uppgift 2

Utvidga klassen så att den får ett nytt attribut typ som skall vara en sträng och kan anta värden "Celsius" eller "Fahrenheit". Gör om alla metoderna så att de tar hänsyn till det nya attributet.

Kommentarer:

1.      I metoden skillnad skall du först kolla att de två inblandade objekten har samma enhet, dvs samma värden på sina typ-attribut, innan differensen beräknas och returneras.

2.      I metoden fahrenheit skall du konvertera till Fahrenheit om grader är givna i Celsius, annars bara returnera grader utan konvertering.

3.      Metoden rapport skall vara deklarerad så här:

4.    void temperatur::rapport( int nedre, int oevre )

dvs kolla om temperaturen är större än oevre grader, mindre än nedre, eller mellan nedre och oevre grader och skriv ut lämplig text.

Uppgift 3

1.      För att få lite mer klarhet i när/hur konstruktorer används, så kan man lägga in utskriftssatser i dessa, t.ex. skriva

2.    cout << "Jag är default-konstruktor: "

3.         << "grader = " << grader << endl;

respektive

cout << "Jag är överlagrad konstruktor: "

     << "grader = " << grader << endl;

Prova! Kompilera och testkör igen!

4.      När man programmerar objekt som innehåller pekare, så måste man även ha en destruktor i klassen. Man kan också annars ha en destruktor som inte gör någonting mer än skriver ut text, bara för att se i vilken ordning som objekt dödas. Dvs lägg till följande destruktor till din temperatur-klass:

5.    temperatur::~temperatur()

6.    {

7.      cout << "Nu dödas objekt med temperatur = "

8.           << grader << endl;

9.    }

OBS! Du skall inte lägga till något extra i main.

Beroende på vilka metoder du använder i main, så kan det se ut som att olika antal objekt skapas och döda. Detta beror på att en s.k. kopieringskonstruktor (copy constructor) skapar objekt ibland lite "bakom kulisserna".

Uppgift 4

Fortsätt på programmet du skapade i uppgift 2, men byt ut hela huvudprogrammet mot ett som innehåller en array juli2002 av sju temperatur-objekt. Har du inget program att utgå ifrån, så se nedan i lösningsförslaget.

·   Skapa arrayen och andra variabler som behövs.

·   Skriv lite ledtext till användaren.

·   Läs in vilken typ (Celsius eller Fahrenheit).

·   Gör en loop och fyll på värden i arrayen, dvs läs antal grader, skapa objekt med rätt typ och grad och tilldela till arrayobjekt.

·   Gör ny loop och skriv ut information om alla objekt i arrayen. Resultatet kan se ut så här (användarens inmatning i fet stil):

·         Fahrenheit eller Celsius?

·         Celsius

·         Ge sju st medeltemperaturer i angiven enhet:

·         15 19 17 22 34 22 21

·         Nu testar vi att skriva ut alla i vacker tabell

·         Temperaturen är 15 grader Celsius.

·         Det är lagom varmt idag.

·         Temperaturen är 19 grader Celsius.

·         Det är lagom varmt idag.

·         Temperaturen är 17 grader Celsius.

·         Det är lagom varmt idag.

·         Temperaturen är 22 grader Celsius.

·         Oh! Vad varmt det är idag.

·         Temperaturen är 34 grader Celsius.

·         Oh! Vad varmt det är idag.

·         Temperaturen är 22 grader Celsius.

·         Oh! Vad varmt det är idag.

·         Temperaturen är 21 grader Celsius.

·         Oh! Vad varmt det är idag.

Själva klassen skall ej ändras, dvs inga nya attribut och metoder behövs.

Lösningsförslag

1.      Uppgift 1:
temperatur1.cpp

2.      Uppgift 2:
temperatur2.cpp

3.      Uppgift 3:
temperatur3.cpp

4.      Uppgift 4:
temperatur4.cpp

 

 

Klasser - Sammanfattning

·   Objektorientering innebär att uppmärksamheten riktas på objekt inom det problemområde uppgiften gäller.

·   Uppgiften löses genom samverkan mellan olika objekt.

·   Programmeringen består i att definiera objekt och deras samverkansformer.

·   Inkapsling: objekten innehåller både data och beteende (funktioner som gör något med data).

·   Den som skriver huvudprogrammet behöver inte veta hur ett objekt, t.ex. en Vara, representeras (vilka attribut den har), bara vilka operationer som finns och hur de ser ut, dvs metoddeklarationer som anger ev. parametrar och ev. returvärde.

·   Att gömma representationen av data med private gör programmeringen mer flexibel. T.ex. kan man ändra representationen av data men ofta ha samma huvudprogram kvar (om ej metoddeklarationerna i metoderna ändrats)! Exempel:
Antag vi vill ha historik över en varas försäljning och inköp till lager, vi kan då införa ett nytt attribut,

·         int antalHistoria[MAX];

     

När t.ex. 100 nya varor av en viss typ köps in så sätts varans Antal-attribut till nytt värde (gamla + 100) samt första lediga plats i historikarrayen sätts till 100. När sedan 34 st av denna vara säljs, så får Antal nytt värde (gamla-34) och historikarrayen får värdet -34 på första lediga plats.
Allt detta kan ske utan att metodernas deklarationer förändras. Även huvudprogrammet är som förut. Det enda som ändrats är att ett nytt attribut lagts in samt innehållet i metoderna modifierats och utökats för att uppdatera det nya attributet.

Studieplan: Pekare i C++

Mål: Att lära sig vad pekare är, hur de används, hur man dynamiskt (under exekveringen) allokerar data, samt, kanske viktigast av allt, hur man undviker problem med pekare.
Läsanvisning: kapitel 5.4 (utom 5.4.8) i "C++ direkt, kapitel 10 (utom 10.4) i "Programmeringsteknik med C++".


Studieplan

·   Läs igenom materialet på kommande webbsidor samt tillhörande programexempel, som kan laddas hem som C++-filer för egna testningar.

·   Utför laborationen.

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen.


Översikt

När man kör ett program finns programmets variabler i datorns primärminne, som är uppdelat i ett antal minnesceller. Dessa celler numreras så att alla får ett unikt nummer. Ett sådant nummer brukar kallas primärminnesadress eller bara adress.

I C++ kan man arbeta med en speciell sorts variabel som innehåller primärminnesadresser. Sådana variabler kallas för pekare (pointer). Antag t.ex. att vi har en variabel k, av typen int, som har värdet 7 och att variabel k råkar finnas på adressen 00012478234. Om vi nu har en annan variabel, p, och denna variabel innehåller talet 00012478234, så kan vi säga att p pekar på k.

    ___ ___        ___ ___ 
   |   |   |      |   |   |
   | p |  -|----->| k | 7 |
   | __|___|      |___|___|

 

När man programmerar vet man oftast inte var i primärminnet en variabel hamnar när programmet exekveras. En variabel kan hamna på olika platser i minnet, och ha olika adresser, varje gång man kör programmet.

Tilldela värden

När man ska ge värden till pekarvariabler anger man därför bara vad pekarvariablerna ska peka på, utan att specificera några exakta adresser. Man arbetar med s.k. relativa adresser. Datorn och operativsystemet kommer automatiskt att omvandla de relativa adresserna till de absoluta adresser som gäller vid just det exekveringstillfället.

Exempel:

int k;
int *p; // deklarera pekarvariabel p
k = 7;
p = &k; // pekarvariabeln p pekar på k

 

 

Pekare - Introduktion

Minnen, celler, adresser och pekare

Låt oss börja med att ställa upp en bild av datorns minne:
en lång rad, en array, av minnesceller som kan innehålla en int, char, double eller någon annan datatyp eller objekt. Detta är inte riktigt korrekt, egentligen innehåller varje cell en byte, och flera byte bygger sedan upp en datatyp. Men som vi skall se behöver man inte bry sig om sådant.

Varje minnescell har en unik adress (här visas en bit av minnet som börjar på adress 1000. I verkligheten brukar variablerna i ett program ligga på betydligt högre adresser):

Tabell 1: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

123

132

131

12

0

45

45

11

19

0

0

0

100

1

47

 

Innan vårt program börjar exekvera ligger det skräp (=vad som helst) i minnet.

Vad händer nu om vi i vårt program deklarerar och initierar en integer myint?

En cell i minnet allokeras (reserveras) då för en integer, och kompilatorn associerar adressen till denna minnescell med symbolen myint. Att cellen allokeras betyder att systemet ser till att inte skriva någonting på den adressen förutom när symbolen myint tilldelas ett värde.

Om vi vill veta själva adressen för den minnescell som är associerad med myint, kan vi använda den unära (=som opererar på ett argument) &-operatorn.

int myint;      // En int-cell allokeras, och 
                // myint associeras med adressen
cout << &myint; // Adressen till cellen skrivs ut 
myint = 5;      // Vi fyller cellen med värdet 5 
cout <<  myint; // Värdet av myint skrivs ut

 

Om vi antar att nästa lediga minnesadress är 1000 kommer minnet efter dessa deklarationer att se ut så här (fet stil anger både här och senare att denna minnescell har allokerats):

Tabell 2: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

5

132

131

12

0

45

45

11

19

0

0

0

100

1

47


och utskriften blir:

1000
5

Vad är en pekare

·   en pekare är en adress till en minnescell

·   datatypen är inte ett vanligt heltal, utan en egen datatyp som är beroende av vad den pekar på

·   en pekare är inte samma sak som en minnescell

·   man kan deklarera en pekare, men den är oanvändbar tills man har satt den att peka på något.

·   när en pekare pekar på något, kan man använda den unära (till skillnad från multiplikationsoperatorn) *-operatorn för att hämta ut innehållet. Exempel: tilldela värdet 6 till det som pekaren myptr pekar på skrivs:

·         *myptr = 6;

 

·   för att sätta en pekare att peka på något, kan man använda &-operatorn (också kallad adressoperatorn). Blanda inte ihop denna operator med referensoperatorn som ser likadan ut.

Exempel på kod i C++

int myint;      // En int-cell allokeras, och
                // myint associeras med adressen 
int *myptr;     // En pekare till int deklareras
                // Den pekar inte på något
myptr = &myint; // myptr pekar på variabeln myint 
myint = 5;      // Vi fyller cellen med värdet 5 
*myptr = 6;     // Vi fyller cellen med värdet 6 
cout <<  myint; // Utskriften blir '6', inte '5' 
cout <<  myptr; // Adressen till cellen skrivs ut 
                // (jämför förra exemplet) 
cout << *myptr; // Innehållet i cellen skrivs ut,
                // dvs '6' 

Om man ritar upp modellen av datorns minne, kan det alltså se ut som nedan. myptr pekar på adressen 1000, som har blivit reserverad att innehålla myint. Dessutom har plats reserverats för variabeln myptr som ligger strax efter myint i minnet (vi antar att en pekare och en int tar lika stor plats). Vi kan alltså som i exemplet ovan komma åt innehållet i cellen på två sätt: genom myint och genom *myptr.

Tabell 3: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

6

1000

131

12

0

45

45

11

19

0

0

0

100

1

47

 

Endast minnescellerna 1000 och 1001 har fått värden, resten av minnet innehåller fortfarande skräp.

Pekare måste initieras

Observera att det inte är vi som bestämmer vilken adress vi får skriva i! Vi kan bara ta emot de minnesadresser som systemet ger oss, eftersom det bara är de som har blivit allokerade. Endast då vet vi

1.      Att vi inte skriver i någon annan data

2.      Att det vi skriver inte kommer att skrivas över med något annat.

Man får alltså absolut inte göra som i exemplet nedan:

int *myptr; // En pekare till int deklareras 
myptr = 20; // Pekaren myptr pekar på adressen 20
            // Vad som finns där, vet vi inte 
*myptr = 5; // Skriver någonting på adressen 20
            // AJ! AJ! AJ! FEL! FEL! FEL!

Felet här ovan kommer helt säkert att orsaka svåra fel, kanske programkrasch (om vi har tur), och visar hur viktigt det är att inte blanda samman pekare till en cell med själva cellen. Om man bara kunde sätta pekare att peka på redan deklarerade variabler som i exemplet ovan, skulle de inte vara så användbara. Vi skall snart se man själv kan allokera minne i datorn.

Pekare som pekar på ingenting

Helst bör alltså pekare sättas att peka på något redan när de deklareras. Kan man inte göra detta skall de ges värdet 0:

int *ipekaren = 0;
double *dpekare = 0;

En pekare som innehåller värdet 0 kallas en tom pekare eller NULL-pekare (null pointer). Egentligen får man inte tilldela heltal till pekare utan typomvandling, men det konstanta värdet 0 är ett undantag. På så sätt har pekarna ett definierat värde även om de inte pekar på något! Anledningen till att detta är så viktigt är att man kan testa om en pekare är 0 innan man använder den:

void funktions_exempel( int *p )
{
  if (p != 0)
    {
      // Pekaren är initierad och 
      // satser med pekaren kan utföras
    }
  else return; // null pointer --> avbryt
               // ty inget vettigt kan göras
}

Pekare är typkontrollerade

Observera att en pekare kan peka endast på variabler av den typ för vilken den deklarerats. Datatyperna "pekare till double" (double*) och "pekare till int" (int*) är skilda typer och variabler av de båda typerna kan inte tilldelas varandra:

int i;
double *dpek = &i // förbjudet!

Detta är naturligt eftersom representation av en "double" i datorn helt skiljer sig från representation av en "int".

Deklaration av pekare

Antag följande deklaration:

int* a, b;
int *c, *d;

Observera att *-tecknet hör till variabeln, inte typen, d.v.s. i deklarationen ovan blir b en vanlig "int", inte en "pekare till en int". Detta kan vara förvirrande eftersom int* faktiskt är ett typnamn. I exemplet ovan har a, b och c typen int*. Var du sätter själva *-tecknet spelar ingen roll, dvs alla tre exemplen nedan anger att variabeln a är en pekare:

·         int *a;

·    

·         int* a;

·    

·         int * a;

·    

Använd gärna "typedef"

Detta att * måste anges för varje pekarvariabel brukar ge upphov till fel. För att reducera felrisken kan man utnyttja sig av en språkkonstruktion som heter typedef.

Om man har en komplicerad typ kan man använda "typedef" för att ge typen ett namn. Man kan sedan använda detta namn i fortsättningen, i stället för att ange den komplicerade typen.

Antag t.ex. att vi vill arbeta med variabler av typerna "pekare till int", men vill undvika fel av typen ovan. Vi kan då ge följande deklaration:

typedef int *intPointer;

En typedef-deklaration har precis samma form som en vanlig variabeldeklaration. Den enda skillnaden är att ordet typedef kommer först. I en typedef-deklaration deklareras inte en variabel, utan ett typnamn. Vi har alltså här deklarerat namnet intPointer. Detta namn kan sedan användas som ett enkelt typnamn på alla ställen där man kan ange en typ. Vi kan t.ex. deklarera variablerna ip1 och ip2:

intPointer ip1, ip2;

Här blir både ip1 och ip2 pekare till "int". Användningen av typedef skapar alltså inte någon ny typ; vi har bara fått ett lättare sätt att deklarera pekare till "int".

Sammanfattning

Pekare är:

·   En pekare är en variabel som har en minnesadress som värde.

·   En pekarvariabel pekar på en viss sorts variabel.

·   En pekare kan peka rakt ut i tomma luften!

·   En pekare kan peka på ingenting (0) - inte likvärdigt med tomma luften

·   En pekare måste alltså initieras innan den används.

·   En pekare deklareras datatyp *namn;

·   Observera att när pekare deklareras hör * till variabeln, ej typen.

Om * och &

I C++ kod

Förklaring

Exempel

* (i deklaration)

pekare till

int *a;

* (i kod)

det som finns där,
värdet

*a

& (i kod före variabel)

adressen till

int b=5; a=&b;

& (mellan datatyp och variabel)

referensen till

void d(int &b)

 

 

 

Pekare - Arrayer

Pekare och arrayer är samma sak!

Något man måste känna till som C++-programmerare är sambandet mellan arrayer och pekare.

Pekararitmetik

Om vi vill deklarera en array av heltal, säg 6 element lång, skriver vi som bekant

int myArr[6]; 

Hur ser vår datorminnesmodell ut nu (vi antar att varje "int" tar upp en minnescell och att arrayen allokeras på minnesadresserna från och med 1002 till och med 1007. Adress 1000 och 1001 har vi ju redan tagit upp i föregående avsnitt).

Tabell 4: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

5

1000

131

12

0

45

45

11

19

0

0

0

100

1

47

 

Vi ser att ytterligare sex minnesceller har allokerats. Adressen till den första associeras med symbolen myArr[0], den andra med myArr[1], osv. Antag nu att vi deklarerar en "int-pekare" och sätter den att peka på det första elementet.

Kan vi då använda den för att komma åt de andra elementen i arrayen? Ja, genom att stega upp pekaren!

int myArr[6];      // Statisk array deklareras
int *myptr;        // Pekare deklareras
 
myptr = &myArr[0]; // Nu pekare myptr på
                   // första elementet i myArr 
 
myptr++;           // Nu pekar myptr på
                   // andra elementet i myArr
myptr++;           // Nu pekar myptr på
                   // tredje elementet i myArr
 
*myptr = 9999;     // Ger det tredje elementet
                   // i myArr värdet 9999

Efter dessa operationer ser minnet ut så här:

Tabell 5: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

5

1000

131

12

9999

45

45

11

1004

0

0

0

100

1

47

 

Vi ser att pekarvariabeln myptr placerats i nästa lediga minnescell (1008) och att det tredje elementet (myArr[2]) fått värdet 9999. Övriga element i arrayen är oinitierade.

Från det ovanstående inser man nu hur en pekare är mer än bara ett heltal. Genom att använda ++-operatorn, stegar vi oss fram en integer i taget, utan att bry oss om hur många bytes en integer tar upp.

Om vi använde vanliga heltal som pekare, skulle vi behöva veta hur många bytes den datatyp vi pekar på tar upp. Det är bl.a. därför som exempelvis en "char-pekare" är en annan datatyp än en "int-pekare". (Den viktigaste skillnaden är att pekartypen avgör hur informationen i minnescellerna skall tolkas. En "char-pekare" som pekar på en integer skulle ge konstiga resultat om man vill läsa eller skriva värden.)

Nu kommer avdelningens huvudpunkt som ett påstående: myArr i exemplet ovan är också den en int-pekare! Vi reviderar bilden av datorminnet ovan. Så här ser det "egentligen" ut:

Tabell 6: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

5

1000

131

12

9999

45

45

11

1002

1004

0

0

100

1

47


D.v.s. variabeln myArr har också fått plats i minnet och "pekar på" det första elementet i arrayen!

Vi har fått sex minnesceller allokerade och myArr är en pekare som pekar på den första minnescellen. Vill vi skriva ut första elementet, är dessa två rader identiska:

cout << myArr[0];
cout << *myArr;

Vi förstår nu varför man börjar räkna på noll i C++. "Nollte elementet" betyder att man har stegat sig fram noll steg. Man kan använda klammernotationen på alla pekare. Exempel:

int myArr[6];  // Statisk array deklareras
int* myptr;    // Pekare deklareras
 
myptr = myArr; // myptr pekar på första elementet
               // Jämför exemplet ovan,
               // raderna gör samma sak! 
 
 
myptr[4]=5;    // Femte elementet i myArr,
               // åtkommet genom myptr 

Också andra aritmetiska operationer är tillåtna på pekare: --, -, +=, t.ex.

Sammanfattningsvis gäller alltså: *(myptr+i) är exakt samma sak som myptr[i].

sizeof och pekare

En skillnad mellan den statiskt allokerade myArr och pekaren myptr är dock att sizeof-operatorn ger olika resultat. sizof(myArr) ger minnesanvändningen för hela arrayen, medan sizeof(myptr) bara ger minnesutrymmet för pekaren.

En skillnad till mellan pekare och arrayer

Ytterligare en skillnad mellan myArr och myptr är att man inte får ändra värdet på myArr (myArr += 2 t.ex. är inte tillåtet), däremot kan man använda den för annan pekararitmetik. Dessa två satser är således identiska:

cout << *(myArr + 2) << endl;
cout << myArr[2] << endl;

Ett råd: använd inte pekaritmetik: skriv myArr[2], inte *(myArr+2). Koden blir mycket läsligare.

Pekare och arrayer som funktionsparametrar

Vi har tidigare deklarerat en funktion som skall ta arrayparametrar så här:

int sum( int a[], int size );

Av de ovanstående bör det ha framgått att nedanstående deklaration är helt ekvivalent:

int sum( int *a, int size );

Dynamisk allokering av arrayer

Ett problem med arrayer är att deras storlekar måste bestämmas vid kodningen. Man måste då ta till "worst case"-utrymme, vilket är slösaktigt med minne. Genom att använda new-operatorn kan man allokera minne (att reservera minnesutrymme för en variabel brukar kallas att allokera minnesutrymme eller bara allokera minne) i körningsögonblicket!. new-operatorn gör två saker:

1.      Den allokerar minne för så många objekt man anger som inargument (eller ett enda objekt om man använder operatorn utan [ ]).

2.      Den returnerar en pekare till det första av dessa objekt.

Exempel:

char *c = new char;    // Minne allokeras
                       // för ett enda tecken
*c = 'a';              // ... som sätts till a
 
int size; 
cin >> size;           // läs in storleken
int* myArr;            // Pekare deklarers
myArr = new int[size]; // Minne allokeras
                       // för size stycken int,
 
// OBS!!! myArr pekar på den första av dessa 
// Nu kan vi använda myArr som en vanlig array 
 
myArr[0]=3; 

Det är mycket ovanligt och oftast slöseri med minne att dynamiskt allokera utrymme för enstaka "char", "int" och liknande (enstaka objekt är en helt annan historia, med det återkommer vi till senare under kursen!).

Att komma ihåg

Precis som i fallet med statiskt allokerade arrayer är det viktigt att komma ihåg att inte skriva utanför arrayen! Det upptäcks inte vid kompilering (naturligtvis), och leder till segmentation fault eller dylikt vid körningen. Situationen i vår datorminnesmodell är precis som i förra illustrationen, men vi har allokerat så många element som vi vill.

När man allokerar minne på egen hand, är det ens eget ansvar att minnet lämnas tillbaka när programmet är slut. En minnesregel är att det för varje new i programmet skall finnas en delete. Operatorn delete lämnar tillbaka minnet. Om man inte gör det, kommer mer och mer minne att reserveras utan att det används. Allokerat minne får inte användas av något annat än det som allokerat det, alltså går minnet förlorat. Man får en minnesläcka. I exemplet ovan skulle vi behöva skriva

// Avallokerar minnet vi reserverade ovan
delete c;
delete[] myArr; 

Klamrarna behövs bara när det handlar om arrayer - men då är det viktigt att man inte glömmer dem!

Sammanfattning

·   Arrayer är en indexerad sekvens av element med samma typ

·   Första elementet har alltid index 0 och dess sista index är arraystorlek-1

·   Varje element kan hanteras som en variabel av arrayens datatyp

·   Vid parameteröverföring: adressen överförs, dvs det ser ut som ett referensanrop (call by reference)

·   En funktion kan aldrig avgöra en arrays längd

·   Eftersom pekare och arrayer är samma sak är funktionsdeklarationerna

o  void myfunc( int a[] );

o  void myfunc( int *a );

ekvivalenta.

Statiska arrayer

·   Deklaration av en endimensionell array:

·         // Deklaration av statisk 1D array
·         datatyp statiskArray[statiskStorlek]; 

·   Minnesutrymme allokeras vid kompileringen

·   Måste ta hänsyn till "worst case"

·   Ineffektivt minnesutnyttjande

Dynamiska arrayer

·   Storleken bestäms vid exekveringen

·   En pekare deklareras, new och delete används för att allokera resp. avallokera minne:

·         datatyp *dynamiskArray; // Pekare deklareras
·          
·         // Allokera minne med hjälp av new-operatorn
·         dynamiskArray = new datatyp[dynamiskStorlek]; 
·          
·         delete[] dynamiskArray; // Minne avallokeras

·   Bättre utnyttjande av minnet

·   Flexibelt

·   Lite krångligare

 

 

Pekare - Flera dimensioner

Flerdimensionella arrayer

Om man vill allokera en dynamisk flerdimensionell array kan man i C++ inte skriva något i stil med:

int *a = new int[3][3]

Detta vore bekvämt, men strukturen för flerdimensionella dynamiska arrayer är som följer: man skaffar sig tre (i detta fall) endimensionella arrayer:

int *arr1 = new int[3];

int *arr2 = new int[3];

int *arr3 = new int[3];

 

Vårt omhuldade minne (som vi nu startar om från 1000 i):

Tabell 7: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

123

132

131

1000

0

45

45

1004

19

0

0

1008

100

1

47

 

Våra tre pekare har hamnat i adresserna 1003, 1007 och 1011. Var och en av dessa pekare pekar på sin lilla array. Hur ska vi nu skaffa oss en tvådimensionell array. Jo, vi måste - innan vi allokerar plats enligt ovan - allokera en dynamisk array av pekarvariabler och sätta de tre elementen i denna att peka på var och en av de tre int-arrayerna. D.v.s. vi skriver så här:

int **arr = new int*[3];

Observera variabeln arr som har två asterisker, dvs *-tecken. Detta kan se märkligt ut men blir kanske klarare om man skriver så här med typedef (vilket är helt ekvivalent med ovanstående):

typedef int *intPointer;

intPointer *arr = new intPointer[3];

Nu ser vi tydligt att vi har allokerat plats för en array av pekare. Nästa steg är att allokera plats för själva data-arrayerna:

int **arr = new int*[3];

for ( int i = 0; i < 3; i++ )

  {

    arr[i] = new int[3];

  }

 

// sätt diagonalelementen till rad/kolumn,

// resten = 0

 

for ( int i = 0; i < 3; i++ )

  {

    for ( int j = 0; j < 3; j++ )

      {

        if ( i==j )

          {

            arr[i][j] = i+1;

          }

        else

          {

            arr[i][j] = 0;

          }

      }

  }

Nu ser vårt minne ut så här:

Tabell 8: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

1004

1007

1010

1000

1

0

0

0

2

0

0

0

3

1

47


d.v.s. variabeln
arr har hamnat i minnescell 1003 och pekar på elementet arr[0] (i minnescell 1000) som själv är en dynamiskt allokerad array och pekar ut arrayen som börjar i minnescell 1004.

Vi kan använda arrayen t.ex. så här:

arr[1][2] = 12;

cout << arr[2][0];

Avallokeringen måste ske i omvänd ordning:

for ( int i = 0; i < 3; i++ )

  {

    delete[] arr[i];

  }

delete[] arr;

Observera att med ovanstående struktur finns det ingenting som hindrar oss från att låta delarrayerna vara olika långa. Vi kan t.ex. skriva så här:

int **arr = new int*[3];

for ( int i = 0; i < 3; i++ )

  {

    arr[i] = new int[i+1];

    for ( int j = 0; j < i; j++ )

      {

        arr[i][j] = i+1;

      }

  }

Minnet får nu följande utseende:

Tabell 9: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

1004

1005

1007

1000

1

2

2

3

3

3

0

0

100

1

47


och vår array får följande struktur:

1

2 2

3 3 3

d.v.s. raderna är olika långa.

 

 

 

Pekare - Referenser

Pekare och referenser

Referenser är också en sorts pekare. Till skillnad från en pekare måste en referens alltid referera något, man kan inte ha 0-referenser eller felaktiga referenser. En referens är ett alias, ett alternativt namn på en variabel, och används oftast i samband med parameteröverföring till funktioner.

Man skall inte använda sig av *-operatorn när man använder referenser.

Användandet av operatorn & kan verka förvirrande. Titta på följande kodsnutt:

void test( int& a ) // & betyder referens

{

  int *apek = &a;   // & betyder adress-operatorn,

                    // d.v.s. adressen till a

  cout << *apek;    // skriver ut a:s värde

  cout << &a;       // skriver ut a:s adress

  cout << a;        // skriver ut a:s värde

}

Notera att & används för både adressoperatorn och för att deklarera referenser.

Exempel: Skillnad mellan referens och pekare

Vi använder ett exempel för att belysa skillnaden mellan referenser och pekare. Nedan är två olika versioner av en funktion som byter värde på två variabler av typen "int".

Den första varianten, byt, använder referenser:

/////////////////////////////////////

// Med referens:

 

void byt( int& a, int& b )

{

  int c = a;

  a = b;

  b = c;

}

När man skall anropa funktionen byt för att byta värden på två variabler i och j kan man göra på följande sätt:

int i = 7;

int j = 9;

byt( i, j ); // i får värdet 9 och j värdet 7

Eftersom den formella parametern a under hela anropet refererar till den aktuella parametern i och på motsvarande sätt den formella parametern b refererar till den aktuella parametern j, så kommer variablerna i och j att ha bytt värden efter anropet till byt.

Vi kan med hjälp av pekare skriva en funktion som utför samma sak (utan att använda referenser). Nedan är en alternativ funktion swap som gör samma sak som ovan:

/////////////////////////////////////

// Med pekare:

 

void swap( int* a, int* b )

{

  int c = *a;

  *a = *b;

  *b = c;

}

Här är parametrarna av typen pekare till int. Vi måste därför använda *-operatorn inne i funktionen för att komma åt det de pekar på. Vid anropet av funktionen swap måste man som aktuella parametrar dessutom ge adresserna till de variabler som ska byta värden. Det kan se ut på följande sätt:

int i = 7;

int j = 9;

swap( &i, &j ); // i får värdet 9 och j värdet 7

Användningen av referenser är betydligt enklare för programmeraren, både inne i funktionen och när funktionen ska anropas.

Använd referensparametrar i stället för pekare när funktionsparametrar ska vara ändringsbara. Det gäller förstås inte fält (arrayer), eftersom dessa alltid överförs via pekare.

 

 

Pekare - Pekarfel

Programmeringsfel med pekare

Pekare är något av det lättaste som finns att göra fel med. Tre vanliga typer av fel är:

1.      Att utnyttja oinitierade pekare:

2.    int *ip;  // (1)

3.    *ip = 12; // (2) Katastrof!!! (exekveringsfel)

     

(1): ip är en pekare som kan peka på heltal, dvs "int", men deklarationen ger den inget värde, den har inget utrymme att peka på än. (2) Om pekarvariabeln inte pekar på något, så kan man inte tilldela detta till 12 t.ex. om inte ip = new int; t.ex. görs innan.

4.      Att utnyttja pekare som pekar på redan frisläppt minne.
Kom ihåg att
delete ip; inte innebär att pekaren tas bort, avlider eller på annat sätt försvinner, det är det som ip pekar på som lämnas tillbaka. Pekaren påverkas inte men saknar vettigt värde. Pekare bör alltid nollställas i samband med avallokeringen:

5.    delete ip;

6.    ip = 0;

     

Det är speciellt lätt att göra fel om man har flera pekarvaribler som pekar på samma minne. Undvik alltså detta!

Observera att det är ofarligt att göra delete på 0-pekare (pekare som har värdet 0). Man behöver alltså inte testa på detta innan man anropar delete.

7.      Att glömma att ta bort minne.
Att allokera nytt minne gång på gång utan att avallokera minne. Kom ihåg att livstiden för dynamiskt allokerade data är från och med skapandet med
new tills dess att den tas bort med delete (vanliga statiska variablers livslängd är ju mellan två { och }, alltså tills dess nuvarande C++-block exekverat färdigt. Minnesläckage:

8.    int* i = new int[1000];

9.    ..

10...

11.// ojdå vi behöver mer minne

12. 

13.i = new int[2000];

14. 

15.// oops, här har vi tappat bort möjligheten

16.// att ta bort minnet vi allokerade förut.

17. 

Så här ska det se ut:

int* i = new int[1000];

..

..

// ojdå vi behöver mer minne

 

delete[] i;

i = new int[2000];

 

 

Pekare - Minnesallokering

Allmänt om minnesallokering

Man kan undra över hur variabler som deklareras statiskt lokalt i en funktion placeras i minnet. C++ har två stora minnesareor tillgängliga: stacken och heapen.

Lokala statiska variabler allokeras på stacken. De allokeras (och initieras) varje gång funktionen anropas och tas bort så fort vi lämnar funktionen. Stacken är en struktur där senast inlagda element tas bort först, på så sätt blir denna datastruktur aldrig fragmenterad. Det är på stacken alla upplagor av rekursiva funktioners data hamnar.

Dynamiskt allokerade variabler placeras på heapen. Eftersom vi kan ta bort (med delete) dessa variabler när som helst kan heapen bli fragmenterad. Allokering på heapen är något långsammare än allokering på stacken.

Observera att vår tidigare minnesmodell (se Tabell 9 i avsnittet "Pekare - Flera dimensioner") alltså inte är helt korrekt. I det sista fallet blir det egentligen så här (vi börjar på minnesadress 2000 i stacken och 1000 i heapen).

Den ursprungliga C++-koden var (se avsnittet "Pekare - Flera dimensioner"):

int **arr = new int*[3];
 
for ( int i = 0; i < 3; i++ )
  {
    arr[i] = new int[i+1];
 
    for ( int j = 0; j < i; j++ )
      {
        arr[i][j] = i+1;
      }
  }

Stacken

Här allokeras variablen arr:

Tabell 10: Illustration av minnet.

 

Adress:

2000

2001

2002

2003

2004

2005

2006

2007

2008

2009

2010

2011

2012

2013

2014

Celler:

1000

45

2

19

245

6788

1001

100

3

16

10

0

1

12

7

 

Heapen

Här allokeras först arrayen arr (1000-1002), sedan de tre arrayerna arr[0] (1003), arr[1] (1004-1005) och arr[2] (1006-1008).

Tabell 11: Illustration av minnet.

 

Adress:

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

Celler:

1003

1004

1006

1

2

2

3

3

3

0

0

0

100

1

47

 

 

Pekare - Exempel

Funktion som returnerar en pekare

Vi har sett exempel på pekare som argument till funktioner, t.ex. swap. Men en funktion kan även returnera en pekare. Följande funktion, min söker efter det minsta talet i en array, och returnerar detta minimum som en pekare till int.

int *min( int *a, int n )

{

  int i;

  int *val = a;             // minsta talet hittills

 

  for ( i = 1; i < n; i++ ) // undersök övriga element

    {                

      if ( *(a+i) < *val )  // nytt min?

        {

          val = a + i;      // spara minsta talet

        }

    }

  return val;               // returnera minsta talet

}

 

Funktionen tar två inparametrar, den första är en pekare till det aktuella fältet och den andra är antalet element som finns i fältet. Med en "for-loop" söker vi efter minsta talet och efter att alla element har jämförts så returnerar vi en pekare till det minsta talet.

Med denna funktion samt den tidigare definierade swap kan vi enkelt skriva en urvalssorteringsfunktion urvals_sortering. Funktionen urvals_sortering sorterar ett heltalsfält i stigande ordning, enligt algoritmen:

1.      Nollställ loop-index, i = 0.

2.      Sök minsta värdet i fältet a[i] - a[n]

3.      Byt plats på det minsta och första värdet.

4.      Öka loop-index med ett, dvs i = i + 1. Upprepa 2. och 3. tills fältet är sorterat.

void urvals_sortering( int *a, int n )

{

  // deklaration av pekare till minsta talet

 

  int *val;

 

  for ( int i = 0; i < n; i++ ) // för alla element

    {

      val = min( a+i, n-i );    // sök minsta talet

      swap( a+i, val );         // byt plats

    }

}

 

Inparametrarna till funktionen ovan är de sammma som för funktionen min, dvs den första är en pekare till vår array och den andra är antalet element i arrayen. En skillnad är dock att efter funktionens slut, så är den inskickade arrayen a sorterad.

Notera att vi minskar sökfältet i anropet till funktionen min, dvs val = min(a+i,n-i), med ett för varje iteration, eftersom den vänstra delen av fältet a blir sorterad. Därefter byter vi plats på första elementet i den osorterade delen av fältet a, dvs a+i, och det minsta elementet val.

Rekursion och pekare

Ett annat intressant exempel på pekare och funktioner, är rekursion. Nedan listar vi en funktion bin_sok som utför binär sökning, med hjälp av rekursion.

Funktionen tar tre inparametrar. Den första, sokt, är det sökta talet, en s.k. söknyckel. Den andra parametern är en pekare till fältets första element och den tredje är en pekare till fältets sista element. Som resultat låter vi funktionen bin_sok returnera en pekare till det element i fältet där det sökta talet finns. Skulle det sökta talet inte finnas i fältet låter vi funktionen returnera värdet 0 (Null-pekare).

int *bin_sok ( int sokt, int *forsta, int *sista )

{

  if ( forsta > sista ) // fältet saknar element

    {

      return 0;         // Null-pointer

    }

 

  // låt pekaren mitten peka på elementet

  // som ligger i mitten av fältet

 

  int *mitten = forsta + (sista-forsta)/2;

 

  if ( sokt < *mitten )      // vänstra halvan?

    {

      // rekursivt anrop med vänstra halvan

 

      return bin_sok( sokt, forsta, mitten-1 );

    }

  else if ( sokt > *mitten ) // högra halvan?

    {

      // rekursivt anrop med högra halvan

 

      return bin_sok( sokt, mitten+1, sista );

    }

  else                  // vi har hittat sokt och

    return mitten;      // returnerar pekaren

}

 

Allra först i funktionen kontrollerar vi specialfallet om pekaren till fältets första element, forsta, pekar till höger om pekaren till fältets sista element, sista. Det innebär att fältet saknar element och således saknas det sökta elementet, sokt, och vi returnerar 0.

Om detta inte är uppfyllt vet vi att fältet innehåller element. Då låter vi pekaren mitten peka på det mittersta elementet i fältet. Eftersom fältet är sorterat i storleksordning kan vi avgöra om det sökta talet ligger i vänstra halvan, högra halvan eller om det är lika med elementet i mitten. I det sista fallet returneras en pekare till det sökta elementet.

Om det sökta talet ligger i vänstra halvan gör vi ett rekursivt annrop till bin_sok, där vi låter pekaren till fältets sista elementet vara mitten-1 (dvs en pekare till det element som ligger till vänster om mittersta elementet). På motsvarande sätt låter vi pekaren till fältets första element vara mitten+1, dvs en pekare till det element som ligger till höger om mittersta elementet, i det rekursiva anropet till bin_sok då det sökta talet ligger i högra halvan.

Antag att vi har ett sorterat fält, array, med 100 element. Då skulle ett anrop till funktionen kunna se ut:

int nyckel = 123;

int *res = bin_sok( nyckel, array, array+99 );

Exempel

Klicka på länken till filen nedan för att se ett fullständigt program med funktionerna ovan. Även körningsexempel kan fås upp i eget fönster.

 

 

Pekare - Laboration

Uppgift 1

Med följande deklarationer skapar vi en heltalspekare b, som pekar på heltalsvariabeln a:

int a = 3;
int *b = &a;

Frågor

Vad är b? Vad är *b? Kopiera stommen till programmet här: stomme.cpp
Skriv följande satser efter deklarationerna, kompilera och testkör för att hitta svaret:

 
cout << "\n b = " << b <<   endl;
cout << "\n *b = " << *b <<endl;
cout << "\n a = " << a << endl;

Skriv ytterligare följande satser i programmet:

int c = 5; b = &c;
cout << "\n b =" << b << endl;
cout << "\n *b =" << *b << endl;

Ändra värdet på *b och skriv ut värdet av c. Vad händer? Kompilera och kör. Kan b peka på en double?

Uppgift 2

Skriv ett program som gör följande:

·   Läs in ett heltal n.

·   Skapa en heltalsarray med plats för n stycken tal.

·   Läs sedan n tal och lagra dem i arrayen.

·   Skriv sedan ut talen med 5 tal per rad.

·   Kopiera arrayen till en annan array.

Uppgift 3

Skriv ett program som gör följande:

·   Skriv en funktion

double *laes ( int &n )

som läser in heltalet n och n st flyttal (double) i en array, och returnerar denna array som en pekare.

·   Skriv en funktion som tar en array som inparameter, och beräknar och returnerar medelvärdet.

double medel ( double *arr, int antal)

·   Skriv ett huvudprogram som testar funktionerna.

Uppgift 4

Skriv ett program som gör följande:

·   Skriv en funktion

double *sort ( const double *arr, int antal)

som returnerar en sorterad array. Talen kommer från inparameterna arr.

Varför har vi deklarerat arr som en const?

·   Skriv en funktion

void skriv ( double *array, int n, int k )

som skriver ut talen i arrayen array med k st tal per rad. n är längden på arrayen.

·   Skriv även ett huvudprogram som testar funktionerna.

Uppgift 5

Skriv ett program som gör följande:

·   Skriv en funktion

double sum ( double *a, int n )

som beräknar summan av arrayen a med hjälp av rekursion.

·   Skriv ett huvudprogram som testar funktionen.

Lösningsförslag

1.      Lösningsförslag till uppgift 1

2.      Lösningsförslag till uppgift 2

3.      Lösningsförslag till uppgift 3

4.      Lösningsförslag till uppgift 4

5.      Lösningsförslag till uppgift 5

 

 

Filer - cin & cout

Vad är cin och cout?

Vi börjar denna diskussion om filer med att titta på de gamla kända cin och cout. Detta har sina poänger: vi kan återanvända mycket av det vi lärt oss om cin och cout när vi skall ge oss in på att skapa och läsa filer.

·   cin är ett fördefinierat objekt av klassen istream

·   cout är ett fördefinierat objekt av klassen ostream

Vi har alltså två olika klassdeklarationer som deklarerar varsin klass. Sedan finns objekten cin och cout deklarerade som instanser (dvs variabler) av respektive klass (allt detta finns i filen iostream, som man inkluderar överst i sina C++-filer).

Vid körning av program

När ett program vill ha ett tal eller en sträng från användaren (den som kör programmet) skriver programmet ofta en ledtext för att hjälpa användaren, men de två objekten cin och cout har ingen direkt kontakt med varandra.

Klassen cin lagrar allt du skriver och när programmet behöver något hämtas det från cins lager - om det finns något att hämta. Annars väntar programmet tills något dyker upp.

Detta betyder bl.a. att vi mycket väl kan svara på datorns frågor innan den hunnit ställa dem!

Läsning med cin

Vid läsning hämtas tecken för tecken från cins lager tills programmet är nöjt. Vad nöjd betyder varierar beoende på vad programmet vill ha.

Formaterad inmatning

Operatorn >> kan användas för att läsa vanliga tal och strängar. För att skilja olika tal och strängar åt använder vi s.k. white space (vita tecken eller separatorer), dvs blank, tab och radmatning. Antag att ditt program innehåller satserna:

string a;
int b, c;
cin >> a >> b;
cin >> c;

och att du när du kommer till inläsningssatsen knappar in:

    Kalle -7<return>
47<return>

dvs du skriver på två rader och trycker på return efter båda. Inledande blanka kastas bort, Kalle hamnar i a, blanktecknet mellan Kalle och -7 kastas bort, -7 hamnar i b, nyradtecknet kastas och 47 hamnar i c.

Observera att det avslutande nyradstecknet finns kvar. Observera också att man inte kan läsa strängar som innehåller blanka på detta sätt.

Detta fungerar bra så länge vi använder >> och inte bryr oss om vita tecken. Men det finns exempel då vi stöter på problem, t.ex. vid inläsning av namn.

Eftersom inläsningen alltid stannar vid nästa vita tecken, kan man inte läsa in ett namn med förnamn och efternamn på detta sätt:

string namn;
cout << "Vad heter du? ";
cin >> namn;

Om jag nu knappar in

Kalle Anka

får strängen namn innehållet Kalle och Anka blir kvar i lagret till nästa inläsning. Detta beror på att inläsningen avbryts när vi stöter på ett vitt tecken, i detta fall det blanka tecknet mellan Kalle och Anka.

Oformaterad inmatning

För att läsa in vita tecken måste vi använda någon metod i klassen istream (eller funktion i klassen string).

  get();
  get( char& s );
  getline( istream& is, string& str );

Metoden get() hämtar nästa tecken från cins lager (behandlar alla tecken lika, inga tecken ses som separatorer) och returnerar detta tecken. Egentligen returnerar denna variant av get faktiskt en "int". Skälet till detta är att man skall kunna få reda på när det är slut på indata, get returnerar då värdet EOF, end of file.

Metoden get(char& s); gör samma sak, men returnerar tecknet som en "char" i form av en referensparameter.

Funktionen getline(istream &is, string &str); läser in en hel rad till strängen str. Den läser även bort radbrytningstecknet (return-tecknet).

Funktionen getline fungerar alldeles utmärkt när vi skall läsa in ett namn med förnamn och efternamn

string namn;
cout << "Vad heter du? ";
getline( cin, namn );

Om vi nu knappar in

Kalle Anka

får strängen namn innehållet Kalle Anka, precis som vi hade tänkt.

Blanda oformaterad och formaterad inmatning

Man kan mycket väl blanda formaterad och oformaterad inmatning, dvs blanda inläsning med operatorn >> och t.ex. funktionen getline. Men man måste tänka efter före...

Vi börjar med ett exempel:

// Filnamn : blanda0.cpp
#include <iostream>
#include <string>
using namespace std;
 
int main()
{
  int a,b;
  string str;
 
  cout << "Ge två tal: ";
  cin >> a >> b;
  cout << "Ge en sträng: ";
  getline( cin, str );
  cout << "Talen var : " 
       << a  << " " << b << endl;
  cout << "Strängen var : "
       << str << endl;
  
  return 0;
}

Vad händer nu om vi skriver våra två tal och trycker på return? Jo, talen läses in som de skall, men return-tecknet lämnas kvar. Sedan skrivs Ge en sträng: varefter alla tecken till första return läses. Men eftersom return-tecknet från första inmatningen ligger kvar i cin-bufferten läses det inte in några tecken alls och vi får inte chansen att skriva strängen. Se nedan (fet text är vad jag skriver in):

Ge två tal: 2 3
Ge en sträng: Talen var : 2 3
Strängen var : 

Vi kan lösa problemet med metoden ignore som tar bort högst ett visst antal tecken fram till och med ett specificerat tecken hittas. cin.ignore(1000,'\n'); tar bort högst 1000 tecken fram till och med nästa return-tecken. Vi kan alltså lägga in denna sats före det sista getline-anropet ovan, se nedan

// Filnamn : blanda1.cpp
#include <iostream>
#include <string>
using namespace std;
 
int main()
{
  int a,b;
  string str;
 
  cout << "Ge två tal: ";
  cin >> a >> b;
  cout << "Ge en sträng: ";
  cin.ignore(1000,'\n');
  getline( cin, str );
  cout << "Talen var : " 
       << a  << " " << b << endl;
  cout << "Strängen var : "
       << str << endl;
  
  return 0;
}

Om vi exekverar programmet får vi följande resultat:

Ge två tal: 2 3
Ge en sträng: Hejsan Kalle Anka
Talen var : 2 3
Strängen var : Hejsan Kalle Anka

Det bör nämnas att det finns ett lättare sätt att hoppa över vita tecken. Förutom metoder finns också s.k. manipulatorer som används på ett litet annat sätt än metoder. Ni har redan sett manipulatorn endl. En annan är ws. Med hjälp av manipulatorn ws kan man hoppa över alla vita tecken.

Detta kan vara mycket användbart om man inte tillåter en tom sträng. I exemplet ovan tillåts att strängen str blir tom, t.ex. om man trycker flera return efter inmatningen av siffrorna. Dessutom behöver man inte vara orolig för att någon skriver in mer än 1000 tecken innan hon trycker return (om det nu är så troligt?).

// Filnamn : blanda2.cpp
#include <iostream>
#include <string>
using namespace std;
 
int main()
{
  int a,b;
  string str;
 
  cout << "Ge två tal: ";
  cin >> a >> b;
  cout << "Ge en sträng: ";
  cin >> ws;
  getline( cin, str );
  cout << "Talen var : " 
       << a  << " " << b << endl;
  cout << "Strängen var : "
       << str << endl;
  
  return 0;
}

När vi kör detta program går det bra att knappa in flera return-tecken innan vi skriver text strängen.

Ge två tal: 2 3
Ge en sträng: 
 
Hejsan Kalle Anka
Talen var : 2 3
Strängen var : Hejsan Kalle Anka

Vad händer nu om vi skriver både de två talen och textsträngen på samma rad? Som ni själva kan se om ni provar programmet kommer då den andra frågan efter det att man skrivit in strängen trots att vi tror att vi läser strängen str efter frågan. Från detta kan vi dra en slutsats om när cins lager fylls på: detta inträffar när man trycker <return>. Notera att detta fungerar inte för programmet blanda1.cpp. Varför?

De exempel vi sett ovan visar dels att cin och cout opererar oberoende av varandra, dels att vi måste tänka oss för om vi blandar inläsning med >> och getline.

Mer om inläsning

Två användbara metoder i istream (kom ihåg att cin är en instans av denna klass) är:

·   peek()
tittar på nästa tecken i lagret utan att ta bort det. Precis som get returnerar peek en int.

·   putback( char c )
lägger tillbaka ett tecken till lagret. Detta är bra om vi upptäcker att vi inte skall ha tecknet. OBS! bara ett tecken.

Metoden peek kan användas för att läsa siffror även om de råkar sitta ihop med en sträng. Se peeking.cpp. Metoderna peek och putback kan vara speciellt viktiga när man skall tolka matematiska uttryck.

Felhantering

Det finns bl.a. följande metoder för att hantera fel

·   good() - sann om inget fel inträffat (mer om detta senare)

·   eof() - sann om vi nått slutet av data (mer om detta senare)
OBS! metoden eof returnerar sant först när man försökt läsa förbi filslutet.

·   clear() - nollställer alla fel (som internt i cin är flaggor, logiska variabler)

Används på följande sätt t.ex. if (cin.good()) ...

Man kan också testa direkt i inläsningen med if (cin >> a) ... Om a är en int så läses enbart heltal. Se exempel testcin.cpp.

Skrivning med cout

Att skriva är enklare eftersom vi har mer kontroll över vad som händer (ingen användare som kan skriva helt fel saker t.ex.). De största svårigheterna har vi redan sett: synkroniseringen av cin och cout

·   Vi använder operatorn << för att skriva vanliga tal eller strängar.

·   Några metoder i ostream kan vara bra att känna till:

o  width( int n ) - bestämmer hur många tecken nästa utskrift skall ta i anspråk.

o  precision( int n ) - anger antal siffror i reella tal

o  setf( long flag ) - sätter en flagga

o  unsetf( long flag ) - nollställer en flagga

Det finns massor av olika flaggor, t.ex. ios::fixed som anger att vi skall skriva med fixt antal decimaler. Om denna flagga är satt betyder precision antal decimaler, annars antal siffror som visas.

·   Var och en av metoderna ovan motsvaras också av en manipulator (för att använda de flesta av dessa måste <iomanip> inkluderas):

o  setw( int n )

o  setprecision( int n )

o  setiosflags( long flag )

o  resetiosflags( long flag )

Ett utskriftsexempelprogram finns i filen manip.cpp.

Det finns det en metod put(char c) i ostream som man kan använda för skrivning av ett tecken (jämför get(char& c) i istream). Man klarar sig dock bra med cout << c som till skillnad från cin >> c kan hantera alla sorts tecken, även t.ex. blanka.

Det finns många metoder och manipulatorer för att styra utskrifter. De som nämnts här är de viktigaste. Efter detta är det dags att gå vidare till filer.

Exempel

Klicka på länkarna till filerna nedan för att se exempelprogram för cin och cout. Även körningsexempel ges.

Filer - introduktion

Varför filer?

Filer är användbara i programmeringssammanhang så snart man vill mellanlagra resultat eller skall hantera stora datamängder (siffror och text t.ex).

Antag att vi har skrivit ett program för hantering av varor i en affär, dvs programmet har koll på antal varor som köps in till lagret och som säljs. När man öppnar affären och slår på de datoriserade kassaapparaterna så önskar man naturligtvis att kassaprogrammet i dessa från början skall ha kontakt med lagerdatabasen som innehåller all information om varor (pris, antal i lager etc). Ingen kassörska skulle börja morgonen med att mata in alla varors namn, pris mm i kassaapparatens program t.ex., utan allt bygger på att man kvällen innan sparade data om varorna i någon form av databas eller fil. Varje morgon låter man kassaprogrammet läsa startdata från föregående kvälls sparade fil etc.

Vad är filer egentligen?

Det finns olika sorters filer. I denna kurs behandlar vi enbart s.k. textfiler, där varje tecken (bokstav, siffra, etc) tar en byte (oftast).

·   En fil är en datamängd med början och slut.

·   Vi läser och skriver filer sekventiellt, dvs ett tecken i taget i en viss ordning, nämligen från början till slut. Detta sker på samma sätt som vi västerlänningar läser en webbsida: vi startar upptill och läser åt höger tills raden är slut, byter rad, fortsätter från vänster till höger.

·   För att kunna läsa från en fil måste den öppnas för läsning.

·   För att kunna skriva till en fil måste den öppnas för skrivning.

·   En fil öppnas antingen för läsning eller skrivning, inte bådadera.

Klasserna ofstream och ifstream

Motsvarande klasserna istream och ostream (som finns definierade i filen iostream) finns för filer också: ifstream och ofstream. Dessa klasser finns definierade i filen fstream som inkluderas på samma sätt som iostream.

Man läser från filer och skriver på filer på liknande sätt som med cin och cout.

 

 

Filer - Läsa filer

Att läsa filer

För att läsa en fil skall vi använda ett objekt av klassen ifstream som görs tillgänglig genom att inkludera filen fstream.

Läsa rad för rad

Ett första exempel som skriver innehållet i en textfil på skärmen (rad för rad):

// Filen copy1a.cpp

#include <iostream> // för cout

#include <fstream>  // för ifstream

#include <string>   // för string

#include <cstdlib>  // för exit

using namespace std;

 

int main()

{

  string rad;

  string filnamn;

  cout << "Ange filnamn: ";

  getline( cin, filnamn );

 

  // Öppna filen för läsning

  // (fin är en instans av ifstream)

  // skapa objektet fin

 

  ifstream fin( filnamn.c_str() );

 

  // Felkontroll

 

  if ( !fin )

    {

      cout << "Filen kunde inte öppnas"

           << endl;

      exit( EXIT_FAILURE );

    }

 

  // läs rad för rad

 

  while ( getline( fin, rad ) )

    {

      // och skriv varje rad

      cout << rad << endl;

    }

 

  return 0;

}

I programmet ovan är filnamn namnet på filen. Detta läses in som en string från tangentbordet, och används som parameter till ifstream-konstruktorn. Notera att vi måste använda string-klassens metod c_str() i anropet till konstruktorn.

Innan vi kan börja läsa från filen, bör man göra en felkontroll för att kontrollera att vi kunde öppna den önskade filen. Om öppningen misslyckades sätts en tillståndsflaggan failbit i ifstream. Ett enkelt sätt att göra detta är att med en if-sats kontrollera om uttrycket !fin returnerar true.

Observera sedan att vi använder objektet fin exakt som om det vore cin! Ett sätt att beskriva detta är att säga att objektet fin är associerat med filen filnamn på samma sätt som cin är associerat med tangentbordet.

Läsa rad för rad med eof-kontroll

Ett alternativ sätt att läsa filer är att använda metoden eof(), end of file. I C++ måste man ha försökt att läsa när filen är slut för att eof() skall returnera false. Detta kan skapa problem om man inte kodar på rätt sätt...

// Filen copy1b.cpp

#include <iostream> // för cout

#include <fstream>  // för ifstream

#include <string>   // för string

#include <cstdlib>  // för exit

using namespace std;

 

int main()

{

  string rad;

  string filnamn;

  cout << "Ange filnamn: ";

  getline( cin, filnamn );

 

  // Öppna filen för läsning

  // (fin är en instans av ifstream)

  // skapa objektet fin

 

  ifstream fin( filnamn.c_str() );

 

  // Felkontroll

 

  if ( !fin )

    {

      cout << "Filen kunde inte öppnas"

           << endl;

      exit( EXIT_FAILURE );

    }

 

  // Läs rad för rad.

  // Läs första gången utanför loopen,

  // så blir det rätt med eof-kontrollen!

 

  getline( fin, rad );

 

  while ( !fin.eof() )

    {

      // och skriv varje rad

      cout << rad << endl;

 

      // försök läsa nästa rad

      getline( fin, rad );

    }

 

  return 0;

}

Programmet copy1c.cpp gör samma sak men med while-loopen ersatt av en for-loop.

Läsa tecken för tecken

Man kan också läsa filer tecken för tecken. Programmet ovan enklare:

// Filen copy2a.cpp

#include <iostream> // för cout

#include <fstream>  // för ifstream

#include <string>   // för string

#include <cstdlib>  // för exit

using namespace std;

 

int main()

{

  string filnamn;

  cout << "Ange filnamn: ";

  getline( cin, filnamn );

 

  // Öppna filen för läsning

  // (fin är en instans av ifstream)

  // skapa objektet fin

 

  ifstream fin( filnamn.c_str() );

 

  // Felkontroll

 

  if ( !fin )

    {

      cout << "Filen kunde inte öppnas"

           << endl;

      exit( EXIT_FAILURE );

    }

 

  // läs tecken för tecken

 

  char c;

  while ( fin.get(c) )

    {

      // och skriv varje tecken

      cout << c;

    }

 

  return 0;

}

Eftersom vi med fin.get läser alla tecken inklusive blanka och nyradstecken kommer filen att kopieras till skärmen.

Läsa tecken för tecken med eof-kontroll

Man kan även läsa filen tecken för tecken genom att göra test på end of file med hjälp av metoden eof().

// Filen copy2b.cpp

#include <iostream> // för cout

#include <fstream>  // för ifstream

#include <string>   // för string

#include <cstdlib>  // för exit

using namespace std;

 

int main()

{

  string filnamn;

  cout << "Ange filnamn: ";

  getline( cin, filnamn );

 

  // Öppna filen för läsning

  // (fin är en instans av ifstream)

  // skapa objektet fin

 

  ifstream fin( filnamn.c_str() );

 

  // Felkontroll

 

  if ( !fin )

    {

      cout << "Filen kunde inte öppnas"

           << endl;

      exit( EXIT_FAILURE );

    }

 

  // läs tecken för tecken med eof-kontroll

 

  char c;

 

  fin.get(c); // Försök läsa första tecknet!

 

  while ( !fin.eof() )

    {

      // och skriv varje tecken

      cout << c;

      // Försök läsa nästa tecken

      fin.get(c);

    }

 

  return 0;

}

Läsa tal för tal

På samma sätt som vi läste in bokstav för bokstav kan vi läsa in tal för tal med operatorn >>, d.v.s. inläsning från fil fungerar precis som inläsning från fil. I detta exempel läser vi in en fil med mätvärden. Först läser vi genom filen en gång för att ta reda på antal värden, sedan skapar vi en array av rätt storlek, och slutligen läser filen en gång till och lägger värdena i arrayen. Vi använder också en ny variant för att testa om filen gick att öppna. (Exempel på datafil: data.txt.)

// Filen lasdyn.cpp

#include <iostream> // för cout

#include <fstream>  // för ifstream

#include <string>   // för string

using namespace std;

 

int main()

{

  double *data;

  int num = 0;

 

  // öppna filen första gången

  ifstream fin("data.txt");

 

  if ( fin.good() )

    {

      double d;

      // räkna antal värden i filen

 

      while ( fin >> d ) num++;

 

      // stäng filen

      fin.close();

 

      // rensa filströmmen

      fin.clear();

 

      // och skapa en dynamisk array

      data = new double[num];

 

      // öppna filen så vi läser från början

      // ev. en ny kontroll?

 

      fin.open("data.txt");

 

      // läs in till arrayen

 

      for (int i=0; i < num; i++)

        fin >> data[i];

    }

  else

    {

      cout << "Kunde inte öppna filen data.txt"

           << endl;

      return 0;

    }

 

  // Skriv ut för att kolla att allt gick bra

 

  for (int i=0; i < num; i++)

    cout << data[i] << endl;

 

  return 0;

}

Notera hur fin.good() används som filkontroll. Om något problem uppstått i filströmmen (d.v.s. fin i exemplet) kommer fin.good() att returnera falskt. I somliga kompilatorer nollställs inte filströmmens variabler när en fil stängs. Om man inte nollställer strömmen innan man öppnar en ny fil med samma ström kommer den då att komma ihåg vad som hände med den gamla filen. Det är vanligt att man avslutade den föregående filen med att nå filslut och det kommer få fin.good() att returnera falskt när man öppnar den nya filen trots att det gick bra att öppna den.

För att vara säker på att detta problem inte uppstår är det en bra idé att anropa fin.clear() i samband med att man stänger sin fil. Vissa kompilatorer gör detta automatiskt, men det är ju tråkigt om koden bara fungerar i vissa kompilatorer. C++-standarden ställer inga krav på att strömmen ska nollställas när man stänger en fil (tyvärr).

Läsa flera filer

En fil öppnas oftast direkt i ifstream-konstruktorn och stängs i motsvarande destruktor. Men man behöver inte ange ett filnamn i ifstream-konstruktor, man kan skapa ett ifstream-objekt som man senare kan koppla till ett filnamn. På samma sätt kan man stänga filen innan destruktorn anropas om man vill. Man använder de två metoderna open respektive close, se lasflera.cpp.

Läsa ord för ord med eof-kontroll

Metoden eof returnerar sant först när man försökt läsa förbi filslutet, vilket kan ställa till med problem. Betrakta följande funktion som skall räkna antal ord (= antal sammanhängande sekvenser av icke-whitespace) i en fil:

// Se filerna antal1.cpp

// och ord

int  antal(string fil)

{

  int ant=0;

  ifstream in( fil.c_str() );

 

  while (!in.eof())

    {

      string temp;

      in >> temp;

      ant++;

    }

 

  return ant;

}

Eftersom eof blir sann först när in >> temp; misslyckats en gång returnerar denna funktion fel värde (antal ord plus 1). Vi kan lätt rätta till detta genom att returnera ant-1. Alternativt skriver vi så här (och testar direkt på läsningen):

// Se filerna antal2.cpp

// och ord

 

int  antal( char fil[] )

{

  int ant=0;

  ifstream in(fil);

  char temp[50];

 

  while (in >> temp)

    ant++;

 

  return ant;

}

Ett tredje sätt är att göra som i exemplen ovan, d.v.s. använda eof-kontroll i while-loopen, men läsa första ordet före loopen. Se antal3.cpp).

Exempel

Klicka på länkarna till filerna nedan för att se exempelprogram för filhantering.

 

 

Filer - Skriva filer

Att skriva filer

Att skriva filer är enkelt (betydligt svårare brukar det vara att veta vad man skall skriva...). Vi öppnar en fil för skrivning genom att skapa ett objekt av klassen ofstream, som finns definierat i biblioteket fstream. ofstream-objektet kan vi sedan behandla precis som cout, bara med den skillnaden att allting kommer att skrivas till filen i stället för till skärmen. Vi kan använda <<-operatorn för att skriva tal och strängar precis som vanligt.

#include   // för ofstream

using namespace std;

 

int main()

{

  ofstream fout( "message.dat" );

  int antalStudenter = 40;

  int antalReserver = 250;

 

  fout << "Välkommen till kursen TDB2" << endl;

  fout << "Du är en av " << antalStudenter

       << " antagna" << endl;

  fout << "Du måste anmäla om du vill" << endl;

  fout << "ha kvar din plats, vi har " << endl;

  fout << antalReserver << " personer till som "

       << endl;

  fout << "också vill gå kursen!" << endl;

  return 0;   

}

 

Filen message.dat får detta utseende:

Välkommen till kursen TDB2

Du är en av 40 antagna

Du måste anmäla om du vill

ha kvar din plats, vi har

250 personer till som

också vill gå kursen!

 

Exempel

Klicka på länken för att arbeta med programmet,

Att lägga till i en redan existerande fil

Om man vill lägga till (append) något till en redan existerande fil utan att ändra på det som filer innehåller från början. Det kan vara frågan om att lägga in en nyantagen student i en lista med studenter. Detta åstadkoms genom att ange flaggan ios::app när man öppnar filen. Om filen inte finns, så skapas den.

Exempel

Klicka på länken för att se exempelprogram append.

 

 

Filer - Strömmar & funktioner

Strömmar som parametrar till funktioner

Antag att vi vill skriva en funktion som kopierar en fil till en annan. Då skulle vi kunna skriva funktionen kopiera, som tar två filnamn (string) som inparametrar:

void kopiera( string &infil, string &utfil )

{

  ifstream fin( infil.c_str() );

  ofstream fout( utfil.c_str() );

  // Ev felkontroll...

  char ch;

  while ( fin.get( ch ) )

    {

      fout << ch;

    }

}

Vi kan anropa funktionen kopiera med följande kodsnutt:

string inFil = "original.txt";

string utFil = "kopia.txt";

kopiera( inFil, utFil );

Ibland är det till fördel att öppna filerna utanför funktionen och bifoga de öppnade filerna som parametrar, t.ex. om man vill skapa en stor fil av flera mindre filer. I C++-kod:

void kopiera( ifstream &fin, ofstream &fout )

{

  char ch;

  while ( fin.get( ch ) )

    {

      fout << ch;

    }

}

För att anropa funktionen kan vi då skriva:

ifstream IN("original.txt");

ofstream UT("kopia.txt");

// Ev felkontroll...

 

kopiera( IN, UT );

 

Ett ännu bättre sätt att skriva funktionen kopiera är att deklarera fin och fout som instanser av klasserna istream respektive ostream.

void kopiera( istream &in, ostream &out )

{

  char ch;

  while ( in.get( ch ) )

    {

      out << ch;

    }

}

Man kan alltså skicka instanser av ifstream och ofstream i stället för istream och ostream. För den intresserade: ifstream ärver istream och ofstream ärver ostream vilket gör detta möjligt - mer om arv kommer senare i kursen.

Notera att instanserna av ifstream, ofstream eller istream, ostream skall referensdeklareras när de är parametrar till en funktion.

Med andra ord kan metoden kopiera användas för att läsa från tangentbordet och skriva på skärmen samt läsa från och skriva till filer:

// deklarationer

ifstream IN;

ofstream UT;

 

// läser från tangentbordet och

// skriver till filen original.txt

UT.open( "original.txt" );

kopiera( cin, UT );

cin.clear(); // Har läst EOF, måste rensa

UT.close();

 

// Kopierar filen original.txt

// till filen kopia.txt

IN.open( "original.txt" );

UT.open( "kopia.txt");

kopiera( IN, UT );

IN.close();

UT.close();

 

// Skriver ut resultatet på skärmen

// genom att läsa filen kopia.txt

IN.open( "kopia.txt" );

kopiera( IN, cout );

IN.close();

Första gången vi använder kopiera så läser vi från tangentbordet med cin. Eftersom funktionen kopiera läser med en while-loop tills EOF påträffas, kommer cin att ha läst EOF. För att kunna använda cin senare i programmet måste vi rensa bort EOF från cin. Detta gör vi med hjälp av cin.clear();. Orsaken till att vi inte behöver göra det för IN är att vi stänger filerna efter varje läsning.

Exempel

Klicka på länkarna nedan för att se fullständiga program för att kopiera och konkatenera (sätta ihop) filer. Även körningsexempel kan fås upp i eget fönster.

Filer - Exempel

Vi har fått i uppgift att skriva en funktion som läser från en fil och ersätter samtliga förekomster av en teckensekvens mot en annan sekvens. De båda sekvenserna kan ha olika längd. Man kan anta att sekvenserna inte sträcker sig över ett radbyte. Antalet rader är okänt och inga antanganden om maximal storlek får göras. Den redigerade texten skall sparas på en annan fil. (Kanske borde vi också kontrollera om det gick bra att öppna filerna.)

//////////////////////////////////////////////////

// Byt alla förekomster av en sträng mot en annan.

#include

#include

#include

using namespace std;

 

//////////////////////////////////////////////////

// Funktionen ersatt ersätter alla förekomster

// av strängen nyckel i in_fil med strängen ny

// och skriver resultatet i filen ut_fil

 

void ersatt( string in_fil, string ut_fil, string nyckel, string ny )

{

  ifstream fin( in_fil.c_str() );

  ofstream fout( ut_fil.c_str() );

  string rad;

  int pos;

  int lnyckel = nyckel.length();

  int lny     = ny.length();

 

  // Vi läser filen radvis

 

  while ( getline( fin, rad ) )

    {

      // för varje rad letar vi efter nyckeln

 

      pos = rad.find( nyckel );

 

      while ( pos >= 0 ) // nyckel hittad

        {

          // byt nyckeln mot ny

 

          rad.replace( pos, lnyckel, ny );

 

          // leta efter nyckel igen

 

          pos = rad.find( nyckel, pos+lny );

        }

 

      // skriv ut den modifierade raden

 

      fout << rad << endl;

    }

}

 

int main()

{

  string sok,  byt;

  cout << "Ange strängen du vill ersätta: ";

  getline( cin, sok );

  cout << "Ange strängen du vill byta till: ";

  getline( cin, byt ); 

  ersatt("GammalFil.txt","NyFil.txt",sok,byt);

  return 0;

}

 

Exempel

Klicka på länken till filen nedan för att kunna testa själv.

Filer - Sammanfattning

Kort sammanfattning om filer

·   I exemplen har fin och fout använts för att påvisa likheterna med cin och cout. Man kan naturligtvis använda vilket identifierarnamn som helst på variabler av klasserna ifstream och ofstream.

·   Vi måste inkludera filen <fstream>

·   Filen öppnas i konstruktorn eller med metoden open

·   Filen stängs i destruktorn eller med metoden close

·   Filer kan läses radvis, teckenvis eller ordvis

Filer - Laboration

Uppgift 1

Dataviruset Reverse har skadat alla textfiler i din dator genom att skriva om raderna baklänges. Exempelvis har en textfil fått följande utseende:

,kcig negoks i ellO allil sroM

kcilb i nekslos hco dnik åp rosor

ålb orä räb vatu åms anrappäL

åg räh masne ås tta ppals gaj araB

 

?räd rasful mev ,murbilemmurB

rä ted tssiv dnuh nE .akank anraksuB

dalg rilb ellO neM .nesläp rä givruL

!gaddoG es ,arb rav ted ,tarmak ne ,Å

Ursprungligen stod det så här i filen:

Mors lilla Olle i skogen gick,

rosor på kind och solsken i blick

Läpparna små utav bär äro blå

Bara jag slapp att så ensam här gå

 

Brummelibrum, vem lufsar där?

Buskarna knaka. En hund visst det är

Lurvig är pälsen. Men Olle blir glad

Å, en kamrat, det var bra, se Goddag!

 

Skriv ett program som du kan använda för att återställa dina filer. Programmet ska först fråga efter namnet på en fil med skadat innehåll, liksom namnet på en ny fil där den återställda texten ska lagras. Därefter ska programmet läsa in den skadade filen, vända på raderna och spara dem i rätt ordning på den nya filen. Radstrukturen skall naturligtvis bibehållas, se exempel ovan. Lösningsförslag finns.

Uppgift 2

Två kompisar (här Eva och Bosse) åker utomlands tillsammans på semester. De turas om att betala för restaurangbesök, bensinkostnader, etc., men bokför noga vem som betalar vad. Båda personerna är med på alla aktiviteter. När de kommit hem till Sverige vill de reda ut ekonomin på ett rättvist sätt, dvs så att båda får betala lika mycket. Slutfrågan är: Vem är skyldig vem pengar och hur mycket?

De lagrar alla data på en textfil, betalat.txt:

Eva

Bosse

Eva     200.00

Bosse   300.00

Bosse   100.00

Eva     100.00

Eva     200.00

De första två raderna anger namnen på de två personerna. Sedan kommer en följd av rader som anger vem som betalat och hur mycket. T.ex. Eva betalade 200 kr då de åt glass, Bosse 300 kr då de tankade bilen etc.

De vill att du skriver programmet som skall se ut så här vid körning (ungefär):

Läser in namn och belopp från filen betalat.txt

Eva har betalat 500 kr totalt.

Bosse har betalat 400 kr totalt.

Resultat: Bosse skall betala Eva 50 kr.

Skriv programmet som läser från betalat.txt och beräknar summorna samt ger utskrift liknande den ovan. De två första raderna i filen anger alltid två olika namn. I exemplet ovan följer sedan fem (5) rader, men programmet skall klara av kvittolistor (filer) av olika längd naturligtvis. Lösningsförslag finns.

Lösningsförslag

1.      Lösningsförslag till uppgift 1

o  reverse.cpp

o  skadad.txt

2.      Lösningsförslag till uppgift 2

o  kvitto.cpp

o  betalat.txt

 

 

 

Studieplan: Klasser & filer

Mål:
Läsanvisning: kapitel 11 i "C++ direkt", kapitel 12, speciellt 12.3 i "Programmeringsteknik med C++". Dock skall vi ej använda "friends" alls. Det är bättre att se till att alla klasser inblandade har s.k. selektorer för att skicka ut attributvärden.

Studieplan

·   Läs igenom materialet på kommande webbsidor + tillhörande programexempel, som kan laddas hem som C++-filer för egna testningar.

·   Läs i kompendiet. Vid behov, ladda hem filer och testa själv.

·   Utför laborationen.

·   Gör andra inlämningsuppgiften om klasser och filer. Bedöm två andra i samma grupp.

·   Invänta lärarens kommentarer på inlämningsuppgiften.

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen.


Översikt

Att läsa in data till attributen i en klass från filer och att skriva ut attributvärden hos en klass till filer kräver inte så mycket extra. Det fungerar i princip som att läsa från tangentbord och skriva på skärm. Ofta brukar man skicka ofstream eller ostream och ifstream eller istream som parametrar (alltid referensparametrar).

Kom ihåg att cin egentligen är ett objekt i klassen istream och cout egentligen är ett objekt i klassen ostream.

Det kan vara bra att låta inläsningsmetoderna returnera själva filströmmen omvandlad till bool, eller returnera false om det är slut på data i filen, dvs "end of file", annars true, så är det lättare att använda metoden från huvudprogrammet eller metod i annan klass som kanske är aggregat t.ex.

 

 

 

Klasser & filer - introduktion

Att skriva ut klassattributvärden på en fil

Skicka filnamnet

Låt oss studera den lilla klassen Person som definieras enligt:

class Person 
{
private:
  string namn;     // personens namn
  int alder;       // personens ålder
 
public:
  Person ();
  Person ( string n, int a );
  void skriv( string filnamn );
};

Ett huvudprogram kan se ut så här:

//----------------------------------------
int main()
{
  Person p("Kalle Anka", 39);
  cout << "Vilken fil skall vi skriva ut på?" << endl;
  string filnamnet;
  getline(cin, filnamnet);
  p.skriv( filnamnet );
 
  return 0;
}

Vilket ger följande körningsresultat:

Vilken fil skall vi skriva ut på?
a.txt

Där namnet på filen anges av den som kör. Här är det filen a.txt som skapas och har följande innehåll:

Personen Kalle Anka med ålder 39

 

Exempel
Utskriftsmetoden har filnamn som parameter.

Skicka strömmen ofstream

Metoden skriv skriver ut namn och ålder i en fil, men ofta vill man nog skriva en mängd personers data i samma fil - då vore det bättre att öppna filen i anropande funktion (t.ex. main) och anropa metoden med redan öppnad fil. Vi skriver om metoden på detta vis. Se nedan.

void Person:: skriv( ofstream &fout )
{
  // Filen förväntas vara öppnad i anropande
  // programmet, här=main.
 
  fout << "Personen " << namn 
       << " med ålder " << alder << endl;
}

Ett nytt huvudprogram kan se ut så här:

int main()
{
  Person p("Kalle Anka", 39);
  cout << "Vilken fil skall vi skriva ut på?" << endl;
  string filnamnet;
  getline(cin, filnamnet);
 
  ofstream utfilen( filnamnet.c_str() );
  p.skriv( utfilen );
 
  // p.skriv( filnamnet );
 
  return 0;
}

 

Exempel
Ström ofstream skickas som parameter.

Om vi vill skriva ut många objekt, t.ex. som ligger lagrade i en array i main, så kan det se ut så här:

int main()
{
  // Skapa en array med personer.
  const int MAX = 5;
 
  Person pers[MAX] = {Person("Kalle Anka", 39),
                            Person("Kajsa Anka", 35),
                            Person("Mimmi Pigg", 25),
                            Person("Musse Pigg", 24),
                            Person("Spöket Laban", 3)};
    
  cout << "Vilken fil skall vi skriva ut på?" << endl;
  string filnamnet;
  getline(cin, filnamnet);
 
  ofstream utfilen( filnamnet.c_str() );
 
  for ( int i = 0; i < MAX; i++ )
    pers[i].skriv( utfilen );
 
  return 0;
}

 

Exempel
Array av personobjekt i huvudprogrammet.

Vill man inte använda ofstream:s konstruktor då man kopplar till fysisk fil (här t.ex. a.txt), så kan man använda metoderna open, close istället:

cout << "Vilken fil skall vi skriva ut på?" << endl;
string filnamnet;
getline(cin, filnamnet);
 
ofstream utfilen;
utfilen.open ( filnamnet.c_str() );
 
for ( int i = 0; i < MAX; i++ )
  pers[i].skriv( utfilen );
 
utfilen.close(); // Behövs ej, stängs automatiskt vid
                 // programmets slut...
                 // Men bra ha denna vana :-)

 

Exempel
Ström ostream skickas som parameter.

Skicka strömmen ostream

Faktum är att man kan (och bör) skriva metoden skriv så här, så blir det mer generellt:

void Person:: skriv( ostream &fout )
{
  // Filen förväntas vara öppnad i anropande
  // programmet, här=main.
 
  fout << "Personen " << namn 
       << " med ålder " << alder << endl;
}

Följande huvudprogram visar att då kan man anropa med cout ibland och utfilen ibland.

...
  ofstream utfilen;
  utfilen.open ( filnamnet.c_str() );
 
  // Skriva ut på fil ibland och cout ibland, t.ex.
  // både och!
 
  for ( int i = 0; i < MAX; i++ )
    pers[i].skriv( utfilen );
 
  utfilen.close(); // Behövs ej, stängs automatiskt vid
                   // programmets slut...
                   // Men bra ha denna vana :-)
 
  for ( int i = 0; i < MAX; i++ )
    pers[i].skriv( cout );
...

 

Exempel

Fråga:
Men varför kan man skicka instanser av ofstream i stället för ostream (annars vore detta ju inte så meningsfullt)?
Svar:
Man kan det därför att ofstream ärver ostream vilket gör detta möjligt. Mer om arv kommer senare i kursen. Med andra ord kan metoden skriv användas för skrivning på skärmen och på filer.

Att läsa in klassattributvärden från en fil

På samma sätt som vid utskrift, så kan man använda istream som parameter till metoder och vid användande (anrop) av metoden välja om det skall läsas från tangentbord, cin, eller från en fil, istream.

För att inläsning från tangentbordet skall bli rätt måste man nog tala om för användaren att programmet väntar sig namnet på en separat rad och åldern på nästa rad t.ex., se nedan:

int main()
{
  // Skapa en array med personer.
  const int MAX = 5;
 
  Person pers[MAX];
 
  cout << "Ge " << MAX << " st personer "
       << "(namn och ålder på olika rader)" << endl;
 
 
  ifstream indata("indatanamn.txt");
 
  // Testa med inläsning från tangentbord
  // eller fil genom att kommentera bort den ena. 
 
  for ( int i = 0; i < MAX; i++ )
    {
    //    pers[i].las( cin );         
    pers[i].las( indata );
    }
  
  cout << "Vilken fil skall vi skriva ut på?" << endl;
  string filnamnet;
  getline(cin, filnamnet);
 
  ofstream utfilen;
  utfilen.open ( filnamnet.c_str() );
 
  // Skriva ut på fil ibland och cout ibland, t.ex.
  // både och!
 
  for ( int i = 0; i < MAX; i++ )
    pers[i].skriv( utfilen );
 
  utfilen.close(); // Behövs ej, stängs automatiskt vid
                   // programmets slut...
                   // Men bra ha denna vana :-)
 
  for ( int i = 0; i < MAX; i++ )
    pers[i].skriv( cout );
 
  return 0;
}

 

Exempel
Huvudprogrammet kan bara läsa in MAX antal st rader och tilldela objekt i arrayen pers. Inläsningsmetoden använder istream, dvs inläsning från tangentbord eller fil är möjligt.

Returnera en bool från inläsningsmetoden

Egentligen brukar det vara smartare att se till att inläsningsmetoderna returnerar en bool, så man ej behöver veta hur många rader som finns i filen! Så här kan det se ut i main då:

.....
  ifstream indata("indatanamn.txt");
 
  // Testa med inläsning från tangentbord
  // eller fil genom att kommentera bort den ena. 
 
  // Läs till filen är slut eller inmantning från
  // tangentbord tar slut (ctrl-D = eof).
 
  Person p;
  int antal = 0;
 
 
  //  while ( p.las( cin ) )
  while ( p.las( indata ) )
    {
      if ( antal < MAX )
           {
             pers[antal] = p;
             antal++;
           }
    }
 
  cout << "Vilken fil skall vi skriva ut på?" << endl;
  string filnamnet;
  getline(cin, filnamnet);
 
  ofstream utfilen;
  utfilen.open ( filnamnet.c_str() );
 
  // Skriva ut på fil ibland och cout ibland, t.ex.
  // både och!
 
  for ( int i = 0; i < antal; i++ ) // Nu med "antal"
    pers[i].skriv( utfilen );
.....

Metoden las kan då se ut så här:

bool Person:: las( istream &fin )
{
  // Namn (för- och efternamn) på en rad
  // ålder på nästa rad.
  // Returnera filströmmen omvandlad till bool, eller
  // returnera "icke end of file", så kan huvudprogrammet
  // loopa på vettigare sätt.
 
  getline( fin, namn );
  fin >> alder;
  fin.get();   // bort med returtecknet...
 
  //    return fin;          // Antingen denna...
  return !fin.eof();   // eller denna.
} 

där båda varianterna av returvärde fungerar för ovanstående huvudprogram.

Exempel
Huvudprogrammet vet ej antalet rader i filen. Inläsningsmetoden returnerar en bool.

Strömmar skall vara referensparametrar

Glöm ej ha strömmar som referensparametrar i C++. Detta gäller både vanliga funktioner och klassmetoder. Egentligen är det lätt att komma ihåg, om man tänker sig att en strömvariabel, t.ex. fin ovan innehåller en osynlig filpekare som håller ordning på hur lång i filen man har läst (eller skrivit). Vid första anropet så läses första raden i filen, vid andra anropet läses andra raden - man börjar ju inte om med den första igen - detta fixas automatiskt av C++.

Filobjektets status ändras vid IO-operationer t.ex. filpekaren samt ett ental s.k. statusflaggor ("end of file" m fl.).

Klasser & filer & pekare

Nedan visar vi exempel med klasser, filer och pekare. Här har vi en variant då man ej läser till "end of file", utan filen själv innehåller information som hjälper till vid inläsningen, så man vet hur länge man skall läsa.

Vi tänker oss en klass som skall lagra mätserier.

Klassen SMatserie: en mätserie, statisk array

Klassen SMatserie skall lagra en mätserie. Här använder vi statisk array.

const int MAXANTAL = 100;

 

class SMatserie

{

 private:

  double d[MAXANTAL];

  int antal;

 

 public:

  SMatserie();

  void laes( istream &is );

  void skriv( ostream &os );

  double medel();

};

Ett huvudprogram kan se ut så här:

int main()

{

  ifstream fin( "matserie" );

  SMatserie vaerden;

  vaerden.laes( fin );

  vaerden.skriv( cout );

  cout << "Medelvärdet är "

       << vaerden.medel()

       << endl;

 

  return 0;

}

Antag nu att filen matserie innehåller:

8 3.4 5.6 1.2 3.5 1.8 6.7 3.4 3.9

där första talet på raden anger antalet tal i mätserien (här=8), sedan kommer alla talen. Körningen blir så här:

8 3.4 5.6 1.2 3.5 1.8 6.7 3.4 3.9

Medelvärdet är 3.6875

 

Exempel
Hela koden finns nedan.

Klassen DMatserie: en mätserie, dynamisk array

Det är ju inte alltid så roligt att ha en fix arraystorlek. Vi modifierar attributen och metoderna lite grand; bl.a. byter vi ut den statiska arrayen mot en dynamisk. Huvudprogrammet behöver inte ändras (mer än att vi måste byta namn på klassen eftersom den nu heter DMatserie). Detta faktum, att man kan byta representationen av data internt i en klass utan att program som använder klassen behöver ändras, är en mycket viktig fördel med objektorienterad programmering: objekten vet själva hur de ser ut inuti men ingen annan behöver eller ska veta. T.ex. inte huvudprogramsprogrammeraren!

Klassen DMatserie ser ut så här:

class DMatserie

{

 private:

  double *d;

  int antal;

 

 public:

  DMatserie();

  ~DMatserie();

  void laes( istream &is );

  void skriv( ostream &os );

  double medel();

};

Eftersom det är dynamisk array inblandad, så har vi new och delete med lite här och där, t.ex. i inläsningen från fil:

void DMatserie:: laes( istream &is )

{

  is >> antal;

  d = new double[antal]; //Så mycket vi behöver bara

 

  for ( int i = 0; i < antal; i++ )

    is >> d[i];

}

 

Exempel
Hela koden finns nedan.

Klassen SMatserier: flera mätserier, statisk/dynamisk 2-dim array

Antag att vi nu vill lagra flera mätserier i vår lilla klass. Vi behöver en array av pekare och allokerar dynamiskt plats för varje mätserie: Här kommer d att fungera som en 2-dim matris med element d[s][i] där 1:a dimensionen är fix=MAXANTAL och 2:a dimensionen är dynamisk.

const int MAXANTAL = 100;

 

class SMatserier

{

private:

  double *d[MAXANTAL]; // 100 st pekare till double.

  int antal[MAXANTAL]; // hör ihop med "d"

  int antalserier;    

 

 public:

  SMatserier();

  ~SMatserier();

  void laes( istream &is );

  void skriv( ostream &os );

  double medel( int serienummer );

  int antalSerier();

};

Huvudprogram:

int main()

{

  ifstream fin("matserier");

  SMatserier vaerden;

 

  vaerden.laes( fin );

  vaerden.skriv( cout );

 

  for ( int i = 0; i < vaerden.antalSerier(); i++ )

    cout << "medelvärdet för serie "

         << i << " är " << vaerden.medel(i) <

där filen matserier ser ut så här:

3

8 3.4 5.6 1.2 3.5 1.8 6.7 3.4 3.9

3 1.2 1.5 1.3

5 5.6 7.8 3.4 5.6 4.5

dvs vi har 3 st mätserier. Den första har 8 värden, den andra har 3 värden och den tredje har 5 värden.

Körningsreultat:

3

8 3.4 5.6 1.2 3.5 1.8 6.7 3.4 3.9

3 1.2 1.5 1.3

5 5.6 7.8 3.4 5.6 4.5

medelvärdet för serie 0 är 3.6875

medelvärdet för serie 1 är 1.33333

medelvärdet för serie 2 är 5.38

Metoden laes:

void SMatserier:: laes( istream &is )

{

  is >> antalserier;

 

  // När vi vet antalet mätserier, så kan vi ju

  // läsa med for-loop.

 

  for ( int s = 0; s < antalserier; s++ )

    {

      is >> antal[s];

 

      d[s] = new double[antal[s]];

 

      for ( int i = 0; i < antal[s]; i++ )

        {

          is >> d[s][i];

        }

    }

}

 

Exempel
Hela koden finns nedan.

Klassen DMatserier: flera mätserier, dynamisk/dynamisk 2-dim array

Nu gör vi dynamisk 2-dimensionell array.

class DMatserier

{

private:

  double **d;  // pekare till pekare (array av pekare)

  int *antal;  // hör ihop med "d"

  int antalserier;    

  void deallocate(); // hjälpmetod, privat

 

 public:

  DMatserier();

  ~DMatserier();

  void laes( istream &is );

  void skriv( ostream &os );

  double medel( int serienummer );

  int antalSerier();

};

Main ser ut på samma sätt, liksom körningsresultatet. Inläsningsmetoden ser nu ut så här:

void DMatserier:: laes( istream &is )

{

  deallocate();

  is >> antalserier;

 

  d = new double*[antalserier];

  antal = new int[antalserier];

 

  // När vi vet antalet mätserier, så kan vi ju

  // läsa med for-loop.

 

  for ( int s = 0; s < antalserier; s++ )

    {

      is >> antal[s];

 

      d[s] = new double[antal[s]];

 

      for ( int i = 0; i < antal[s]; i++ )

        {

          is >> d[s][i];

        }

    }

}

 

Exempel
Hela koden finns nedan.

Det här sista exemplet är ganska klurigt med alla pekare. Nästan på gränsen till överkurs!

 

 

 

Klasser & filer - aggregat

Nedan visar vi exempel med klasser, filer och aggregat. Vi skall studera klassen Lagenhet som består av ett antal Rum.

Man får tänka lite olika i lösningsförfarandet beroende på om klassen Rum har egna metoder för inläsning och skrivning på fil eller ej.

Fakta: En lägenhet består av ett kök, ett badrum samt ett antal andra rum. Ett rum har egenskapen "yta" i kvadratmeter.

Utan egen inläsnings- och utskriftsmetod

Utan inläsningsmetod i den "innersta klassen" (Rum), dvs den som den yttre klassen (Lagenhet) har objektattribut av.

Klassdefinitioner

const int MAXANTALRUM = 10;
 
class Rum 
{
private:
  int yta;
  
public:
  Rum();
  Rum( int y );
  int hamtaYta();
};
 
class Lagenhet 
{
 private:
  Rum koek;
  Rum badrum;
  Rum rum[MAXANTALRUM]; // Andra rum
  int antalrum;         
  
 public:
  Lagenhet();
  Lagenhet( int antrum );
  Lagenhet( int koeksyta, int badrumsyta, 
            int antalrum, int rumsyta[]);
  bool laes( istream &is );
  void skriv( ostream &os);
};

 

Huvudprogrammet

const int MAXANTALLGH = 20;
 
int main()
{
  Lagenhet lagenhet[MAXANTALLGH];
  ifstream fin("lagen_in.txt");
  ofstream fout("lagen_ut0.txt");
  int antal = 0;
 
  if ( fin )
    {
      while ( antal < MAXANTALLGH &&
                 lagenhet[antal].laes(fin) )
           {
             antal++;
           }
      
      for ( int i = 0; i < antal; i++ ) 
           {
             lagenhet[i].skriv(fout);
           }
    }
  else
    cout << "Infilen fanns kanske inte..." << endl; 
  return 0;
}

Dvs vi öppnar filerna i huvudprogrammet och skickar med strömmarna till inläsnings- och utskriftsmetoderna i klassen Lagenhet.

Indatafilen

För varje lägenhet finns följande information i filen:

koeksyta badrumsyta antalrum yta för varje rum

Filen lagen_in.txt ser ut så här:

20 12 3 20 10 10
12 12 2 15 10

 

Metoden laes

bool Lagenhet::laes( istream &is )
{
  // För varje lägenhet finns 
  // följande information i filen: 
  // koeksyta badrumsyta antalrum yta 
  // för varje rum.
  //  Exempel: 
  // 20 12 3 20 10 10
  // 12 12 2 15 10
 
  // Rum har ingen egen inläsningsmetod.
  // Använd temporär variabel vid
  // inläsning. Om det gick bra:
  // använd konstruktor i Rum-klassen.
 
  int temp;      // temporär variabel
 
  if (!(is >> temp)) 
    return false;  
  koek = Rum(temp);
 
  if (!(is >> temp)) 
    return false;  
  badrum = Rum(temp);
 
  if (!(is >> temp)) 
    return false;
 
  antalrum = temp;
 
  for ( int i = 0; i < antalrum; i++ )
   {
     if (!(is >> temp)) 
       return false;
     rum[i] = Rum(temp);     
   }
  return true;
}

 

Man kan tycka att det ser krångligt ut, men för att ha ett vettigt program så måste man ha koll på att man har data kvar i filen som man läser ifrån, dvs så det ej är "end of file". Därav alla

if ( !(is >> temp) )
  return false;
// Annars kommer vi hit. Gick det bra, så använd temp.
...

Om inläsningen går bra så får variabeln temp detta värde. Om end of file har inträffat så returneras false, annars används värdet som temp har. Man skulle lika gärna kunna skriva så här:

is >> temp;
if ( is.eof() ) return false;
// Annars kommer vi hit. Gick det bra, så använd temp.
...

Hur som helst, man ser att den överlagrade konstruktorn till klassen Rum används för att skapa värden till attribut i Lagenhet-klassen.

Metoden skriv

void Lagenhet::skriv( ostream &os )
{
  // Rum har ingen egen utskriftsmetod.
  // Måste använda selektorer.
 
  os << "Kök med yta: " << koek.hamtaYta() << " ";
  os << "Badrum med yta: " << badrum.hamtaYta() << endl;
  os << "Antal rum : " << antalrum << " med ytor: ";
 
  for ( int i = 0; i < antalrum; i++ )
    {
      os << " " << rum[i].hamtaYta();
    }
 
  os << endl;
}

 

Utdatafilen

Filen lagen_ut0.txt ser ut så här:

Kök med yta: 20 Badrum med yta: 12
Antal rum : 3 med ytor:  20 10 10
Kök med yta: 12 Badrum med yta: 12
Antal rum : 2 med ytor:  15 10

 

Exempel
Hela koden finns nedan.

Med egen inläsnings- och utskriftsmetod

Vi kan alternativt låta Rum ha en egen inläsningsmetod och utskriftsmetod. Huvudprogrammet påverkas inte av denna interna förändring i klasserna, vi lägger till lite i Rum-klassen och modifierar lite i metoderna i Lagenhet-klassen. Det är viktigt att komma ihåg och inse denna fördel med inkapsling av data och algoritmer inom objektorienterad programmering.

Klassdefinition

class Rum 
{
private:
  int yta;
  
public:
  Rum();
  Rum( int y );
  int hamtaYta();
  bool laes( istream &is );
  void skriv( ostream &os);  
};

Klassen Lagenhet ser likadan ut som nyss (ovan).

Metoderna Rum:: laes/skriv

bool Rum:: laes( istream &is )
{
  // Läs in och returnera true/false beroende
  // på hur inläsningen går.
 
  is >> yta;
  return is; // Eller "return !is.eof();" 
}
 
void Rum:: skriv( ostream &os)
{
  os << " Rum med yta = " << yta;
}

 

Metoderna Lagenhet:: laes/skriv

Med hjälp av Rum-klassens läsmetod kan vi skriva Lagenhet-klassens läsmetod:

bool Lagenhet::laes( istream &is )
{
  // För varje lägenhet finns 
  // följande information i filen: 
  // koeksyta badrumsyta antalrum yta 
  // för varje rum.
  //  Exempel: 
  // 20 12 3 20 10 10
  // 12 12 2 15 10
 
  // Rum har nu en egen inläsningsmetod.
  // Använd denna istället för konstruktor!
 
  if ( !koek.laes(is) ) 
    return false;
 
  if ( !badrum.laes(is) ) 
    return false;
 
  if ( !(is >> antalrum) ) 
    return false;
 
  for ( int i = 0; i < antalrum; i++ )
    {
      if ( !rum[i].laes(is) ) 
           return false;
    }
 
  return true;
}

Med hjälp av Rum-klassens skrivmetod kan vi skriva Lagenhet-klassens skrivmetod:

void Lagenhet::skriv( ostream &os )
{
  // Rum har ingen egen utskriftsmetod.
  // Måste använda selektorer.
  
  os << " Kök: ";
  koek.skriv( os );
  os << " Badrum: ";
  badrum.skriv( os );
  os << endl;
 
  os << " Antal rum : " << antalrum << ":" << endl;
 
  for ( int i = 0; i < antalrum; i++ )
    {
      rum[i].skriv( os );
    }
 
  os << endl;
}

Utdatafilen

Så här ser resultatfilen ut:

 Kök:  Rum med yta = 20 Badrum:  Rum med yta = 12
 Antal rum : 3:
 Rum med yta = 20 Rum med yta = 10 Rum med yta = 10
 Kök:  Rum med yta = 12 Badrum:  Rum med yta = 12
 Antal rum : 2:
 Rum med yta = 15 Rum med yta = 10

 

Exempel
Hela koden finns nedan.

 

 

Klasser & filer - aggregat

Nedan visar vi exempel med klasser, filer och aggregat. Vi skall studera klassen Lagenhet som består av ett antal Rum.

Man får tänka lite olika i lösningsförfarandet beroende på om klassen Rum har egna metoder för inläsning och skrivning på fil eller ej.

Fakta: En lägenhet består av ett kök, ett badrum samt ett antal andra rum. Ett rum har egenskapen "yta" i kvadratmeter.

Utan egen inläsnings- och utskriftsmetod

Utan inläsningsmetod i den "innersta klassen" (Rum), dvs den som den yttre klassen (Lagenhet) har objektattribut av.

Klassdefinitioner

const int MAXANTALRUM = 10;
 
class Rum 
{
private:
  int yta;
  
public:
  Rum();
  Rum( int y );
  int hamtaYta();
};
 
class Lagenhet 
{
 private:
  Rum koek;
  Rum badrum;
  Rum rum[MAXANTALRUM]; // Andra rum
  int antalrum;         
  
 public:
  Lagenhet();
  Lagenhet( int antrum );
  Lagenhet( int koeksyta, int badrumsyta, 
            int antalrum, int rumsyta[]);
  bool laes( istream &is );
  void skriv( ostream &os);
};

 

Huvudprogrammet

const int MAXANTALLGH = 20;
 
int main()
{
  Lagenhet lagenhet[MAXANTALLGH];
  ifstream fin("lagen_in.txt");
  ofstream fout("lagen_ut0.txt");
  int antal = 0;
 
  if ( fin )
    {
      while ( antal < MAXANTALLGH &&
                 lagenhet[antal].laes(fin) )
           {
             antal++;
           }
      
      for ( int i = 0; i < antal; i++ ) 
           {
             lagenhet[i].skriv(fout);
           }
    }
  else
    cout << "Infilen fanns kanske inte..." << endl; 
  return 0;
}

Dvs vi öppnar filerna i huvudprogrammet och skickar med strömmarna till inläsnings- och utskriftsmetoderna i klassen Lagenhet.

Indatafilen

För varje lägenhet finns följande information i filen:

koeksyta badrumsyta antalrum yta för varje rum

Filen lagen_in.txt ser ut så här:

20 12 3 20 10 10
12 12 2 15 10

 

Metoden laes

bool Lagenhet::laes( istream &is )
{
  // För varje lägenhet finns 
  // följande information i filen: 
  // koeksyta badrumsyta antalrum yta 
  // för varje rum.
  //  Exempel: 
  // 20 12 3 20 10 10
  // 12 12 2 15 10
 
  // Rum har ingen egen inläsningsmetod.
  // Använd temporär variabel vid
  // inläsning. Om det gick bra:
  // använd konstruktor i Rum-klassen.
 
  int temp;      // temporär variabel
 
  if (!(is >> temp)) 
    return false;  
  koek = Rum(temp);
 
  if (!(is >> temp)) 
    return false;  
  badrum = Rum(temp);
 
  if (!(is >> temp)) 
    return false;
 
  antalrum = temp;
 
  for ( int i = 0; i < antalrum; i++ )
   {
     if (!(is >> temp)) 
       return false;
     rum[i] = Rum(temp);     
   }
  return true;
}

 

Man kan tycka att det ser krångligt ut, men för att ha ett vettigt program så måste man ha koll på att man har data kvar i filen som man läser ifrån, dvs så det ej är "end of file". Därav alla

if ( !(is >> temp) )
  return false;
// Annars kommer vi hit. Gick det bra, så använd temp.
...

Om inläsningen går bra så får variabeln temp detta värde. Om end of file har inträffat så returneras false, annars används värdet som temp har. Man skulle lika gärna kunna skriva så här:

is >> temp;
if ( is.eof() ) return false;
// Annars kommer vi hit. Gick det bra, så använd temp.
...

Hur som helst, man ser att den överlagrade konstruktorn till klassen Rum används för att skapa värden till attribut i Lagenhet-klassen.

Metoden skriv

void Lagenhet::skriv( ostream &os )
{
  // Rum har ingen egen utskriftsmetod.
  // Måste använda selektorer.
 
  os << "Kök med yta: " << koek.hamtaYta() << " ";
  os << "Badrum med yta: " << badrum.hamtaYta() << endl;
  os << "Antal rum : " << antalrum << " med ytor: ";
 
  for ( int i = 0; i < antalrum; i++ )
    {
      os << " " << rum[i].hamtaYta();
    }
 
  os << endl;
}

 

Utdatafilen

Filen lagen_ut0.txt ser ut så här:

Kök med yta: 20 Badrum med yta: 12
Antal rum : 3 med ytor:  20 10 10
Kök med yta: 12 Badrum med yta: 12
Antal rum : 2 med ytor:  15 10

 

Exempel
Hela koden finns nedan.

Med egen inläsnings- och utskriftsmetod

Vi kan alternativt låta Rum ha en egen inläsningsmetod och utskriftsmetod. Huvudprogrammet påverkas inte av denna interna förändring i klasserna, vi lägger till lite i Rum-klassen och modifierar lite i metoderna i Lagenhet-klassen. Det är viktigt att komma ihåg och inse denna fördel med inkapsling av data och algoritmer inom objektorienterad programmering.

Klassdefinition

class Rum 
{
private:
  int yta;
  
public:
  Rum();
  Rum( int y );
  int hamtaYta();
  bool laes( istream &is );
  void skriv( ostream &os);  
};

Klassen Lagenhet ser likadan ut som nyss (ovan).

Metoderna Rum:: laes/skriv

bool Rum:: laes( istream &is )
{
  // Läs in och returnera true/false beroende
  // på hur inläsningen går.
 
  is >> yta;
  return is; // Eller "return !is.eof();" 
}
 
void Rum:: skriv( ostream &os)
{
  os << " Rum med yta = " << yta;
}

 

Metoderna Lagenhet:: laes/skriv

Med hjälp av Rum-klassens läsmetod kan vi skriva Lagenhet-klassens läsmetod:

bool Lagenhet::laes( istream &is )
{
  // För varje lägenhet finns 
  // följande information i filen: 
  // koeksyta badrumsyta antalrum yta 
  // för varje rum.
  //  Exempel: 
  // 20 12 3 20 10 10
  // 12 12 2 15 10
 
  // Rum har nu en egen inläsningsmetod.
  // Använd denna istället för konstruktor!
 
  if ( !koek.laes(is) ) 
    return false;
 
  if ( !badrum.laes(is) ) 
    return false;
 
  if ( !(is >> antalrum) ) 
    return false;
 
  for ( int i = 0; i < antalrum; i++ )
    {
      if ( !rum[i].laes(is) ) 
           return false;
    }
 
  return true;
}

Med hjälp av Rum-klassens skrivmetod kan vi skriva Lagenhet-klassens skrivmetod:

void Lagenhet::skriv( ostream &os )
{
  // Rum har ingen egen utskriftsmetod.
  // Måste använda selektorer.
  
  os << " Kök: ";
  koek.skriv( os );
  os << " Badrum: ";
  badrum.skriv( os );
  os << endl;
 
  os << " Antal rum : " << antalrum << ":" << endl;
 
  for ( int i = 0; i < antalrum; i++ )
    {
      rum[i].skriv( os );
    }
 
  os << endl;
}

Utdatafilen

Så här ser resultatfilen ut:

 Kök:  Rum med yta = 20 Badrum:  Rum med yta = 12
 Antal rum : 3:
 Rum med yta = 20 Rum med yta = 10 Rum med yta = 10
 Kök:  Rum med yta = 12 Badrum:  Rum med yta = 12
 Antal rum : 2:
 Rum med yta = 15 Rum med yta = 10

 

Exempel
Hela koden finns nedan.

 

 

 

Klasser & filer - laboration

Uppgift 1

Fortsätt med temperaturprogrammet från laborationen Klasser - intro. Länk till lösningsförslag nedan.

Utvidga klassen temperatur från laborationen, nämligen temperatur2.cpp, med följande metoder:

1.      void skriv( ostream &ut ) som skriver ut (lagrar) ett objekt av klassen temperatur på en fil, dvs attributvärden skrivs ut.

2.      void lasin( istream &in ) som läser in (hämtar) ett objekt av klassen temperatur från en fil, dvs inläsning av attributvärden för ett objekt sker.

Skriv även ett huvudprogram som testar dessa metoder. Eftersom cout är ett objekt (dvs en vanlig ström/filvariabel) av klassen ostream så kan vi anropa metoden skriv på följande sätt:

      t1.skriv(cout);
      

och anropet gör att objektet t1 skrivs ut på skärmen. Samma gäller cin för inläsning. Så vi behöver kanske inte de gamla metoderna skriv och lasin. Ofta har man flera inläsnings- och utskriftsmetoder i allafall, t.ex. för kompakt utskrift.

Algoritm huvudprogram:

1.      Skapa en array av temperaturobjekt.

2.      Skriv ut arrayens värden på skärmen och på fil.

3.      Stäng filen för utskrift, öppna den för inläsning.

4.      Läs värden från filen till en array av temperatur-objekt.

5.      Skriv ut arrayvärden på skärmen.

Kanske ej så roligt huvudprogram, men iallafall...

OBS! Metoden lasin läser alltså in temperaturdata från enbart en rad i filen. Denna metod antar att filen redan är öppnad för läsning i main. Inläsning av många temperaturdata sker i main genom att man anropar temperatur-klassens lasin-funktion om och om igen (dvs rad för rad läses attributdata in) tills end of file påträffas.

En klurighet här är att lasin-metoden är en void-metod. Inläsningen får ske på samma sätt som när man gör "klassisk while-loop vid inläsning av tal". Antag vi testar på positiva tal istället för "end of file" nu.

double x;   // temporär variabel som hjälp
double arrayen[MAX];
int antal = 0;
 
cin >> x;
 
while ( antal < MAX && x > 0 )
  {
    // Gör ngt med x
    arrayen[antal] = x;
    antal++;
      
    cin >> x;  // Läs nytt värde
   }

dvs skapa ett temporärt temperatur-objekt i huvudprogrammet, enTemp

1.      Läs värden till enTemp

2.      Loopa så länge det ej är "end of file"

o  Gör ngt med inläst objekt (tilldela arrayelement, uppdatera "antal")

o  Läs in data till objekt enTemp igen

Uppgift 2

Ändra i koden för uppgiften ovan så att lasin-metoden returnerar en bool, dvs sant om filen ej är slutläst, men falskt om den är det.

Skriv om huvudprogrammet och testa metoden.

Uppgift 3

Denna uppgift är en inledande övning till inlämningsuppgift 3.

Klassen Blomgrupp representerar ett antal blommor av en viss art (given av namnet Tulpan, Flox, etc.) placerade på ett givet ställe i trädgården, dvs i position (x,y) i ett visst antal.

Klassdefinition:

#include 
#include 
#include 
using namespace std;
 
class Blomgrupp 
{
private:
  int x, y;       // Positionen (x,y) i trädgården
  string namn;    // Namn på blomgruppen (ex. tulpan)
  int antal;      // Antal st blommor i blomgruppen
 
public:
  Blomgrupp();
  Blomgrupp ( string namnin, int antalin,
              int xin, int yin );
 
  //--Returnerar antalet blommor i denna blomgrupper.
  int hamtaAntal();
 
  //--Returnera sant om blomgr. finns i (xx,yy).
  bool liggeri( int xx, int yy ); 
 
  //--Returnerar sant om blomgruppen heter n.
  bool sammanamn( string n );
 
  //--Läser in data om blomgrupp från fil.
  bool laes( istream &is );
 
  //--Skriver info om blomgruppen.
  void skriv( ostream &os );                   
};

Antag att filen blommor0.txt ser ut så här:

Flox              30  1          1
Flox              30  1          2
Flox              30  2          2
Flox              30  2          1
Tulpan            20  2          5
Rödros            2   13         10
Lilja             10000          14         145
Solros            4   4          3
Blåklocka      6      6          6
Blåklocka      6      5          6
Blåklocka      6      4          6

Körning av ett huvudprogram, som enbart testar inläsnings- och utskriftsmetoderna laes, skriv kan se ut så här:

Vad vill du göra?
1) Läsa från fil och skriva på skärmen.
2) Läsa från fil och skriva på ny fil (ny.txt).
1
 
Blomgrupp: Flox 1 1 30
Blomgrupp: Flox 1 2 30
Blomgrupp: Flox 2 2 30
Blomgrupp: Flox 2 1 30
Blomgrupp: Tulpan 2 5 20
Blomgrupp: Rödros 13 10 2
Blomgrupp: Lilja 14 145 10000
Blomgrupp: Solros 4 3 4
Blomgrupp: Blåklocka 6 6 6
Blomgrupp: Blåklocka 5 6 6
Blomgrupp: Blåklocka 4 6 6

eller utskrift på fil:

Vad vill du göra?
1) Läsa från fil och skriva på skärmen.
2) Läsa från fil och skriva på ny fil (ny.txt).
2
Ny fil (ny.txt) skapas!

Då ser ny.txt ut så här:

Blomgrupp: Flox 1 1 30
Blomgrupp: Flox 1 2 30
Blomgrupp: Flox 2 2 30
Blomgrupp: Flox 2 1 30
Blomgrupp: Tulpan 2 5 20
Blomgrupp: Rödros 13 10 2
Blomgrupp: Lilja 14 145 10000
Blomgrupp: Solros 4 3 4
Blomgrupp: Blåklocka 6 6 6
Blomgrupp: Blåklocka 5 6 6
Blomgrupp: Blåklocka 4 6 6

 

Blomgrupp, laes, skriv

Du skall skriva metoderna för inläsning och utskrift, förutom standardkonstruktor. Metoderna skall vara generella i den meningen att man skall kunna läsa från tangentbord eller fil och kunna skriva på skärm eller fil. Skriv ett huvudprogram som testar att läsa från fil samt att skriva till både skärm och fil.

Inläsningen

Metoden laes läser en rad i filen och tilldelar Blomgrupp-objektet attributvärden. Metoden returnerar false om filen, efter inläsning en rad, är slut, annars returneras true. Inläsning i huvudprogrammet måste ske på sådant sätt att man skall kunna läsa från "vilken blomfil som helst", dvs programmeraren skall ej veta antaler rader i filen! programmet skall läsa tills filen är slut.

Blomgrupp, hamtaAntal, liggeri, sammanamn, hamtanamn

Implementera också den överlagrade konstruktorn och resten av metoderna; de kommer att behövas i inlämningsuppgift 3.


Lösningsförslag

1.      Uppgift 1:
temp2fil.cpp

2.      Uppgift 2:
temp2filbool.cpp

3.      Uppgift 3:
blommor.cpp

 

 

Studieplan mm: OOP - introduktion

Mål: Att lära sig huvudprinciperna inom objektorienterad programmering, t.ex. vad som menas med aggregat, association och arv.
Läsanvisning: kapitel 6 (plus kapitel 9.5 för den som vill fördjupa sig i ämnet polymorfi) i "C++ direkt, kapitlen 9, 11 i "Programmeringsteknik med C++".


Studieplan

·   Läs igenom materialet på kommande webbsidor.

·   Läs i kompendiet.

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen. ==>Genvägslänkar dit....


Översikt

Citat från boken Eriksson: Objekt-orienterad programutveckling med C++:

Den objektorienterade modellen ger ett system som är en bra modell av "verkligheten", mer modulariserat, enklare att förändra och bygga ut, eventuellt genererar återanvändbara komponenter, gör utvecklaren mer produktiv samt har en enhetlig terminologi och modell i alla delar av systemets livscykel.

Citat från boken Skansholm: C++ direkt:

Vid OOP försöker man skriva korrekta, effektiva, återanvändbara och ändringsbara program genom att låta de enskilda modulerna i ett program utgöras av objekt--objekt som möjliggör inkapsling. För att veta vilka objekt som behövs, så utgår man från den design som tidigare gjorts.

 

 

OOP - introduktion

Nedan tar vi mycket kortfattat upp vad det här med objektorientering egentligen är, samt inför nya begrepp som hör till.

Klassbegreppet

Vad är en klass, vad är ett objekt?

Svar:

En klass kan ses som en datatyp eller en slags mall. En klass:

·   beskriver en egendefinierad datatyp.

Klassdefinition
=
representation (data) + operationer som kan utföras på objekt i klassen (medlemsfunktioner, metoder).

 

·   är en mall eller ett mönster. Klassen beskriver hur en samling objekt med samma uppbyggnad och egenskaper ser ut.

Skilj på klass och objekt! Följande göller för objekt:

·   Ett objekt:

o  är en modell av ett verkligt eller tänkt föremål.

o  är en slags variabel, själva förekomsten.

o  har tillhörande attribut och metoder. Det har en begränsad livstid: den skapas (med konstruktorer), används/ändras (med metoder), dödas (med destruktorn).

 

·   Ett objekt som tillhör en klass sägs utgöra en instans av klassen.

·   Man definierar en klass, men brukar ofta skapa, dvs instansiera många objekt (instanser) av klassen.

·   Objekten är fristående; en operation på ett objekt påverkar endast detta objekt. Dock gäller att objekt i samma klass har samma "operationer", metoder, medlemsfunktioner.

Vad är objektorientering

Svar:

·   Ett av de senaste "inneorden" i datavärlden. Både C++ och Java är objektorienterade språk.

·   Man baserar uppbyggnaden av ett programsystem på de objekt som ingår i systemet istället för att utgå från de funktioner som systemet skall uppfylla.

·   Programmet består av ett antal väl avgränsade enheter som kallas objekt.

·   Objektorienterad programutveckling är ett slags modellbygge. Består av tre faser (iterativt):

1.      Objektorienterad analys (OOA):
Vad skall göras?

2.      Objektorienterad design (OOD):
Hur skall det göras?

3.      Objektorienterad programmering (OOP):
Implementera systemet!

Objektorienteringens tre principer:

Man brukar säga att det finns tre huvudprinciper inom objektorientering:

·   Inkapsling: Man gömmer objektets attribut inne i objektet; oåtkomliga utifrån och kan bara förändras av objektet självt. På engelska: Information hiding.

·   Återanvändbarhet: Program uppbyggd på färdiga, väl uttestade komponenter medför att programutvecklingen:

o  har lägre kostnad

o  går snabbare

o  får högre kvalité

 

·   Mångformighet: innebär överlagring av funktioner och annat smart...

Objektorientering i praktiken:

I praktiken innebär objektorientering följande:

·   Använd h-filer och cpp-filer!
Ändringsbarheten ökar om programmet är uppbyggt av flera självständiga moduler (klasser).

·   Ej luddigheter!
Låt varje modul utföra en speciell, väl definierad uppgift.

·   Ändring av program = utbyte av moduler.

·   Uppfinn inte hjulet igen! Vänj dig vid återanvändning.

·   Generella metoder! En programkomponent är återanvändbar om den är anpassningsbar till olika förhållanden (=olika sorters problem). Undvik utskrift/inläsning i metoder som ej direkt är avsedda för utskrift/inläsning.

·   Lär dig förpackningsteknik! Utnyttja inkapsling. Undvik att använda selektorer i onödan, använd istället metoder med objekt som parameter.

OOP - Vad är OOA, OOD, OOP?

Givet ett problem som skall lösas, t.ex. skriva program (skapa ett system) för en internetbank.

Om man löser problemet med objektorienterad programutveckling, så brukar man dela upp de olika faserna i processen i analysfasen, designfasen och implementeringsfasen.

Objektorienterad analys (OOA) = Vad skall göras?

Analysen av problemområdet fokuserar på följande frågor:

Vilken information skall systemet kunna hantera?
Hur kommer systemet att användas?

Svaret är viktigt eftersom modellen inom problemområdet ger ett språk för att uttrycka krav på systemet, t.ex. krav på systemets funktioner och gränssnitt. Principen är att man modellerar den "verkliga världen" (internetbanken) så som framtida användare kommer att se den. Man skaffar en överblick först och fyller sedan på med detaljer.

Följande ingår i denna del i processen:

·   sätta sig in i problemet

·   förstå förutsättningar

·   göra en första, grov, idealiserad modell av programmet

·   finna objekt (substantiv) och operationer (verb)

·   beskriva attribut, fastställa relationer (aggregat, association, arv) mellan objekt (iterativt)

·   förutse, underlätta för kommande ändringar

·   scenarier (användningsfall)

·   dokumententation i form av diagram

·   systemutvecklingsmetod = arbetsschema, symbolspråk, t ex OMT = Object Oriented Modeling Technique eller UML = Unified Modeling Language.

Objektorienterad design (OOD) = Hur skall det göras?

Givet en analys av problemet, definiera och prioritera kriterier för lösningsprocessen. Nyckelfråga här är:

Hur skall problemet lösas?

Princip: en god design har inga större svagheter. Dvs man skall kunna beskriva ett system så att osäkerheter försvinner. Hit hör följande:

·   planera den objektorienterade programmeringen (ganska flytande mellan analys- och designfasen).

·   konkretisera modellen i analysfasen, lägg till detaljer - objekt för objekt; delas in i

o  systemdesign: kommunicera med omgivning, vilket OS?, vilket fönsterhanteringssystem? hantering av avbrott,...

o  objektdesign: utgå från objekten i analysfasen, ange algoritmer, hur skall objektoperationer utföras? parametrar? Använda redan färdiga objekt?

o  Vilket programspråk? OOP: C++, JAVA, etc.

Objektorienterad programmering (OOP) = Implementera/koda program

Egentligen är objektorienterad programmering bara att koda det som kom ut som resultat ur designfasen. Dvs realisera ``analys & design'' i form av ett bra, körbart datorprogram:

·   korrekt: felfritt (minskar underhållskostnaden)

·   effektivt: utnyttja datorresurser väl (ex. realtidssystem)

·   återanvändbart: billigare i längden

·   ändringsbart: byt ut en hel ``modul'', inkapsling

Programmeringsprocessen

Ibland liknas processen vid följande schema:

1.      Intention, vision.

2.      Specifikation.

3.      Analys och design.

4.      Implementation.

5.      Verifikation.

6.      Produktion.

7.      Modifikation.

Specifikation

Hur indata avbildas på utdata.

Beställarens önskemål = kravspecifikation.

Andra krav:

·   portabilitet

·   användarvänlighet

·   effektivitet (CPU-tid och minnesutrymme)

·   noggrannhet

·   robusthet

Dokumentation

Skall skrivas från början och innefatta alla faser i processen.

·   användarmanual:

o  skrivs i förväg

o  bygger på scenarier

o  återförs på beställaren

·   systemdokumentation:

o  design

o  implementation

o  testning (testfall, resultat)

Problem: att hålla aktuell!

Verifikation

·   testning mot specifikationen

·   kan bara påvisa fel

·   förutsägbara testresultat

·   svårt att testa många krav

OOP - relationer

I OOP försöker man identifiera objekten och klasserna + bestämma relationer mellan objekt och klasser.

T.ex.:

Hur skall vi låta objekt samarbeta och integrera för att uppnå den mest korrekta, flexibla och bästa lösningen?

Fem typer av relationer

Det finns fem olika typer av relationer mellan objekt och klasser:

1.      klass-objekt

2.      användning

3.      association

4.      aggregat

5.      arv

Alla dessa har en liten "testmening", dvs en meningen som kan hjälpa programmeraren att specificera vilken slags relation hans/hennes objekt har till t.ex. ett annat objekt, vilket man måste ha koll på före kodningsfasen.

De tre sista är de klart viktigaste, eller mest utmärkande, för objektorienterade programspråk.

"Klass-objekt"-relation (förekomst av)

Följande gäller för denna relation:

·   "Klass-objekt"-relationen existerar alltid för ett objekt, dvs ett objekt kräver att det existerar en klass, eftersom objekt är instans av klassen.

·   Klassen beskriver typen på objektets samtliga dataelement, operationer + implementering av dessa.

·   Testmening: objektet är en "förekomst av" klassen. Klassen klassificerar objektet.

·   Exempel med C++-kod, då vi antar att en klass BankKonto finns och att den har attribut som motsvarar namn på kontoägare och saldo på kontot.

·         BankKonto Lisas;

·         BankKonto Olles("Olle Olsson", 12300);

 

Användningsrelation (utnyttjar)

Följande gäller för denna relation:

·   Det är den vanligaste relationen mellan objekt.

·   Relationen innebär att ett objekt använder ett annat objekt genom att utföra en operation på det.

·   Man kan se det som att objekten som är inblandade samarbetar med varandra.

·   Testmening: "utför operation på", "använder", "utnyttjar".

·   Exempel med kod i C++, då vi antar att klasserna Person (med ett namn-attribut) och Bil (med ett attribut som anger årsmodell) finns deklarerade.

·         Person jag("Nisse Olsson"); 

·         Bil volvo(1999);              

·         ...

·         jag.koer_framaat( volvo );

 

Associationsrelation mellan två objekt (känner till)

Följande gäller för denna relation:

·   Relationen innebär en koppling mellan objekten (enkel- eller dubbelriktad).

·   Ett objekt kommunicerar med ett annat objekt, utan att vara uppbyggt av detta andra objekt.

·   Det finns en pekare från ett objekt till ett annat.

·   Relationen kan vara vara "en till en", "en till många" eller "många till en".

·   Man kan ha associationer mellan objekt i samma klass. Exempel: länkade listor.

·   Denna relation skall ej blandas ihop med aggregat...

·   Testmening: "har koppling till", "känner till", "för varje X finns ett Y", "pekar på".

·   Exempel. En bil kan ha en person som ägare. Detta implementeras som att Bil-klassen har ett attribut som är pekare till objekt som hör till Person-klassen.

·         class Person

·         {

·         ...

·         };

·          

·         class Bil

·         {

·         private:

·           int aarsmodell;

·           Person *foerare_p;

·          

·         public:

·           Bil();

·           void faaEnfoerare( Person *pers_p );

·           ...   

·         };

 

Aggregatrelation (har)

Följande gäller för denna relation:

·   Kallas även nästlade klasser.

·   Ett objekt är uppbyggt av andra objekt och använder dessa för att tillhandahålla sin funktionalitet.

·   Ett objekt har ett attribut som är objekt.

·   Relationen kan studeras utifrån det omslutande objektet (som innehåller andra objekt) eller utifrån det inneslutna objektet (som ingår i ett annat objekt).

·   Skall ej blandas ihop med association...

·   Testmening: "består av", "innehåller", "har".

·   Exempel med kod i C++. En bil har en bensintank, en motor och fyra hjul. En bok består av kapitel. En bil har inte en ägare, för den är inte uppbyggd av ägare! (se association).

·         class bil

·         {

·         private:

·           string typ;           //Biltyp: "Volvo"

·           int framhjuls;        //Fram/bakhjuls-

·                                   driven

·           bensintank tank;      //Bensintanken

·           daeck hf, vf, hb, vb; //Fyra st hjul

·                                 //med däck på

·         public:

·         ...

·         };

Denna bil har alltså fem attribut som är objekt hörande till två andra klasser, nämligen: bensintank och daeck.

Arvsrelation (är)

Följande gäller för denna relation:

·   Det är en unik relation för objektorienterade språk.

·   En klass har vissa allmänna egenskaper som kan vara gemensamma med andra klasser.

·   Arv sker mellan klasser och inte mellan objekt.

·   Vid definition av ny klass, subklass ärver man egenskaper från en annan existerande klass basklass och beskriver sedan skillnaden gentemot basklassen.

·   Testmening: "Är en", "är ett".

·   Exempel med kod i C++: En bil är ett fordon, sparkonto är ett bankkonto, en hund är ett djur.

·         class djur

·         {

·         protected:

·           string namn;

·           string ras;

·         public:

·           ...

·         };

·         class hund: public djur

·         {

·         private:

·           int typ;   // t.ex. vakthund, sällskapshund

·         public:

·         ...   

·         };   

Här har vi att ett objekt hörande till klassen hund egentligen också hör till klassen djur och därför ärver egenskaper från denna klass. Hundar har alltså tre egenskaper: namn, ras och typ.

OOP - polymorfi

Detta är mest lite allmänbildning. I TDB2 kommer vi ej fördjupa oss så mycket inom detta.

Lite om polymorfi (mångformighet)

Polymorfi betyder mångformighet, t.ex. överlagring av funktioner/metoder och s.k. generiska programenheter (mallar, templates). Vi kommer återkomma till en slags polymorfi i avsnittet om arv, men visar lite här nedan också.

·   Överlagring innebär att en programkonstruktion som ser ut på ett visst sätt kan innebära olika saker (t.ex. anrop av olika funktioner/klassmetoder), beroende på typerna av de ingående operanderna. Jfr. överlagrade konstruktorer.

·   Generisk programenhet är en generell mall som programmeraren kan utgå ifrån för att på enkelt sätt skapa programkod. Kan användas för både funktioner och klasser. Kodbesparande!

Exempel: överlagring

Exempel med kod i C++ för att visa överlagring. Antag att vi har klassen Bil:

class Bil
{
private:
  int aarsmodell;
 
public:
  Bil(){aarsmodell = 0;}
  Bil( int in ){ aarsmodell = in; }
  int haemta_aarsmodell(){ return aarsmodell; }
  void Skriv(){ cout << "Bil av årsmodell: " 
                    << aarsmodell << ". "; }
};

Exempel på funktioner med samma namn, men olika datatyper:

int max( int tal1, int tal2 );
double max( double tal1, double tal2 );
Bil max( Bil aarsmodell1, Bil aarsmodell2 );

Litet huvudprogram som visar anrop av funktioner:

int main()
{
  int a = 2, b = 11;
  double ett = 12.44, tvaa = 56.99;
  Bil volvo(1999), opel(1980);
 
  cout << "Max av " << a << " " 
       << b << " = " 
       << max( a, b ) << endl;
 
  cout << "Max av " << ett << " " 
       << tvaa << " = " 
       << max( ett, tvaa ) << endl;
 
  volvo.Skriv(); opel.Skriv(); cout << endl;
  cout << "Max = ";
  max( volvo, opel ).Skriv();
  cout << endl;
  return 0;
}

med tillhörande funktionsimplementationer:

int max( int tal1, int tal2 )
{
  if ( tal1 > tal2 ) return tal1;
  else return tal2;
}  
 
double max( double tal1, double tal2 )
{
  if ( tal1 > tal2 ) return tal1;
  else return tal2;
}
 
Bil max( Bil aarsmodell1, Bil aarsmodell2 )
{
  if ( aarsmodell1.haemta_aarsmodell() >
       aarsmodell2.haemta_aarsmodell() )
    return aarsmodell1;
  else
    return aarsmodell2;
}

Körning ger:

Max av 2 11 = 11
Max av 12.44 56.99 = 56.99
Bil av årsmodell: 1999. Bil av årsmodell: 1980. 
Max = Bil av årsmodell: 1999.

Vi ser att C++ själv känner av vilken max-funktion som skall användas.

Exempel: mallar

Exempel på mallen swap som byter värden mellan två variabler.

template
void swap( Type &a, Type &b )
{
  Type temp = a;
  a = b;
  b = temp;
}
 
int main()
{
  Bil volvo(1989), saab(1999);
  int tal1 = 1, tal2 = 2;
 
  volvo.Skriv();  saab.Skriv(); cout << endl;
  swap(volvo, saab);
  volvo.Skriv();  saab.Skriv(); cout << endl;
 
  cout << tal1 << " " << tal2 << endl;
  swap(tal1,tal2);
  cout << tal1 << " " << tal2 << endl;
  return 0;
}

Körning ger:

 
Bil av årsmodell: 1989. Bil av årsmodell: 1999. 
Bil av årsmodell: 1999. Bil av årsmodell: 1989. 
1 2
2 1

Dvs funktionen är generellt skriven och kan anropas med antingen bilar eller heltal som parametrar.  

Polymorfi inom arv i C++

Vid arv utgår man från en redan existerande klass, s.k. superklass, och härleder en subklass. Om vi har klassen Figur, som beskriver en generell geometrisk figur och har attributen x, y, så kan en subklass vara Cirkel, som dels ärver attributen från superklassen, men dels har sitt eget attribut radie. En annan härledd klass, Rektangel, kanske har sina attribut laengd, bredd. Härledda klasser ärver alla attribut och metoder från superklassen.

Antag att vi har metoder med samma namn, yta, fast på olika nivåer i hierarkin. Ytor beräknas ju på olika sätt för en rektangel jämfört med en cirkel.

I C++ gäller att alla typer av figurer (som är subklasser till Figur) kan lagras i variabler av typen Figur (egentligen pekare till Figur, se arvskapitlet senare i kursen). Olika figurer kan sedan "bete sig" olika när en metod anropas, därför att C++ automatiskt under körningen känner av vilken av metoderna yta som skall användas. Detta kallas polymorfi och är en mycket viktig del av objektorienterad programmering. Programmeraren uppnår denna polymorfi genom att ersätta metoder i basklassen: man skriver helt enkelt en ny metod med samma namn och samma parametrar, fast olika innehåll.

              Figur -->yta()
              /    \
             /      \
yta()<-- Cirkel   Rektangel -->yta()

 

 

OOP - laboration

Uppgift

Vilka typer av relationer kan vi identifiera mellan klasser och objekt? Definiera relationerna mellan nedanstående begrepp på lämpligt sätt, t.ex. genom att rita pilar eller använda följande kodtabell:

->   Arv

..>  Association

::>  Användning

||>  Aggregat

==>  Instantiering

Lista av begrepp:

Hus, person, trappa, rum, möbel, dörr,

fastighetskötare, stol,

handtag, fönster, toalett,

skrivsal, våning, MIT-huset,

korridor, lås, toalettstol,

föreläsningssal, kateder, arkitekt.

 

Lösningsförslag

Förslag på lösning till uppgiften.

 

 

OOP - sammanfattning

Allmänt om objektorienterade språk

De centrala begreppen inom objektorientering är klass och objekt. Det kan vara värt att redogöra för hur dessa relaterar sig till varandra för att undvika missförstånd i framtiden.

En klass definierar en datatyp i C++. Man kan efter att en klass definierats skapa variabler av denna datatyp. Klasser innehåller data (=attribut) och metoder, men en klass i sig kan inte göra något, den endast definierar hur variablerna skall se ut, dvs som en slags mall.

Variabler som har en viss klass som datatyp kallas objekt. Objekten innehåller data, och man kan kalla på metoder som objekt har. Man säger att ett objekt är en instans av en viss klass. Då man skapar ett objekt instantierar man en klass.

Vaför skall man använda objektorienterad programmering

Objektorientering skapar fördelar för både utvecklare och slutanvändare. De förstnämnda eftersom objektorientering förenklar skapandet av programvara och gör arbetet effektivare, slutanvändaren därför att programmen blir mer lättanvända och arbetar bättre tillsammans.

Detaljer

·   Inkapsling ==> färre problem med "globala variabler" och att man ej vet "vem ändrar vad var?". Tack vare att implementationen är inkapslad behöver man inte veta hur någonting görs på detaljnivå, utan det räcker med att veta att det görs korrekt för att klassen skall kunna återanvändas. Detta är egentligen användbart först vid riktigt stora program, när man skriver små program (som i denna kurs), så kan det vara svårt att se dessa fördelar.

·   Genom att använda aggregat, association och arv, så kan man bygga vidare på klasser som redan existerar, dvs återanvändning ==> bättre ekonomi.

·   Uppdelning av koden i klasser till headerfiler och implementationsfiler gör det också lättare att underhålla ==> bättre ekonomi.

·   Modellen ligger närmare problemet ==> även användare/beställare kan vara med i analysen. Lättförstått. Detta behöver ej vara sant, speciellt inte då man kommit långt i sin programmering och inte bara använder klasser för att beskriva "verkliga föremål", utan något mer komplext. Men ofta är detta en fördel.

OOP - sammanfattning

Allmänt om objektorienterade språk

De centrala begreppen inom objektorientering är klass och objekt. Det kan vara värt att redogöra för hur dessa relaterar sig till varandra för att undvika missförstånd i framtiden.

En klass definierar en datatyp i C++. Man kan efter att en klass definierats skapa variabler av denna datatyp. Klasser innehåller data (=attribut) och metoder, men en klass i sig kan inte göra något, den endast definierar hur variablerna skall se ut, dvs som en slags mall.

Variabler som har en viss klass som datatyp kallas objekt. Objekten innehåller data, och man kan kalla på metoder som objekt har. Man säger att ett objekt är en instans av en viss klass. Då man skapar ett objekt instantierar man en klass.

Vaför skall man använda objektorienterad programmering

Objektorientering skapar fördelar för både utvecklare och slutanvändare. De förstnämnda eftersom objektorientering förenklar skapandet av programvara och gör arbetet effektivare, slutanvändaren därför att programmen blir mer lättanvända och arbetar bättre tillsammans.

Detaljer

·   Inkapsling ==> färre problem med "globala variabler" och att man ej vet "vem ändrar vad var?". Tack vare att implementationen är inkapslad behöver man inte veta hur någonting görs på detaljnivå, utan det räcker med att veta att det görs korrekt för att klassen skall kunna återanvändas. Detta är egentligen användbart först vid riktigt stora program, när man skriver små program (som i denna kurs), så kan det vara svårt att se dessa fördelar.

·   Genom att använda aggregat, association och arv, så kan man bygga vidare på klasser som redan existerar, dvs återanvändning ==> bättre ekonomi.

·   Uppdelning av koden i klasser till headerfiler och implementationsfiler gör det också lättare att underhålla ==> bättre ekonomi.

·   Modellen ligger närmare problemet ==> även användare/beställare kan vara med i analysen. Lättförstått. Detta behöver ej vara sant, speciellt inte då man kommit långt i sin programmering och inte bara använder klasser för att beskriva "verkliga föremål", utan något mer komplext. Men ofta är detta en fördel.

Studieplan: Klasser - aggregat

Mål:
Läsanvisning: kapitel 9 och 11 (???) i "Programmeringsteknik med C++".


Studieplan

·   Läs igenom materialet på kommande webbsidor + tillhörande programexempel, som kan laddas hem som C++-filer för egna testningar.

·   Läs i kompendiet. Vid behov, ladda hem filer och testa själv.

·   Utför laborationen.

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen.


Översikt

En klass kan vara attribut (datamedlem) i en annan klass. Detta kallas att den klass som har andra klasser som medlemmar är aggregat av klasser. Ibland säger man att man har nästlade klasser.

Aggregat - introduktion

Vi har hittills använt objekt i väldigt enkla omgivningar, t.ex. inte alls använt komposition av objekt, dvs nästlade klasser eller aggregat. Det innebär att en klass innehåller objekt av samma eller andra klasser som datamedlemmar. Man kan i C++ nästla objekt i princip hur djupt som helst. I större program har man ofta många nivåer av nästlade klasser.

Att nästla en klass i den privata delen av en annan klass kan också vara ett bra sätt att gömma klasser som bara används inom just denna klass och som andra klasser inte ska se.

Har redan sett aggregat...

Eftersom datatypen string egentligen är en klass, så har vi redan sett aggregatexempel. T.ex. vårt första exempel: Vara-klassen:

class Vara
{
private:
  string Namn;       // Namn är egentligen
                     // objekt i klassen
                     // String
  double Pris;
  int    Antal;
 
public:
  void LaesIn();
  void TillLager( int AntalNya );
  void Saelja( int AntalSaelj );
  void ProcentPris( double Faktor );
  void SkrivUt();
};

Dock har vi aldrig behövt tänka extra på den, utan använt den som om det vore en enkel datatyp som t.ex. double.

Aggregat i C++

·   En klass eller objekt är uppbyggd av andra klasser eller objekt.

·   En klass har attribut av andra, redan deklarerade, klasser.

·   Man brukar använda initieringslistor i konstruktorerna, t.ex. antag att attributen i klassen klasssnamn heter d1, d2, .... Då har vi:

·         klassnamn::klassnamn( parameterlista )
·         : d1(uttryck1), d2(uttryck2), ...
·         {
·           \\ Ev. tilldelningar.
·         }

 

Mer om initieringslistor

·   Programkroppen { } måste vara med, även om den är tom.

·   Oftast är initieringslista samma sak som att göra tilldelning, dvs

·         klassnamn::klassnamn( parameterlista )
·         {
·           d1 = uttryck1;
·           d2 = uttryck2;
·           ... etc
·         }

 

·   I vissa fall måste man ha initieringslistor (attribut som är konstanter eller referenser t ex, eftersom dessa ej får tilldelas överkurs).

·   initieringslistor kan ge effektivare kod när man initierar attribut som är objekt.

Aggregat - konstruktorer

Exempel på konstruktorer

Klassen daeck representerar ett bildäck

Låt klassen daeck representera bildäck.

const double MAXdjup = 20.00; // 2cm
 
class daeck
{
private:
  double djup;   // Mönsterdjup, mm gummi
public:
  daeck();
  daeck( double indjup );
  double haemta_djup();
  int slitage( double slitdjup );
  void skriv();
};

Då kan konstruktorerna se ut så här med tilldelning:

daeck::daeck()
{
  djup = MAXdjup;
}
 
daeck::daeck( double indjup )
{
  djup = indjup;
}

eller så här med initieringslista:

daeck::daeck()
: djup(MAXdjup)
{}
 
daeck::daeck( double indjup )
: djup(indjup)
{}

 

Implementation av metoder i bildäcksklassen:

//---Selektorer:
double daeck::haemta_djup()
{
  return djup;
}
//---Andra metoder:
int daeck::slitage( double slitdjup )
{
  // Slit ned "slitdjup" mycket av
  // gummit (om det går).
  // Returnera 1 om allt gummi slut,
  // annars 0.
 
  int slutgummi = 0;
 
  if ( djup - slitdjup < 0 )
    {
      djup = 0;
      slutgummi = 1;
    }
  else
    djup -= slitdjup;
 
  return slutgummi;
}
 
void daeck::skriv()
{
  cout.setf( ios::fixed ); // snyggare utskrift
  cout << "däck = "
       <<  setprecision(1) << djup;
}

 

Klassen bil representerar en bil

Låt oss nu skapa klassen bil som har fyra bildäck. Senare kommer den ha flera attribut, t.ex. bensintank, x-position, framhjulsdriven,...

class bil
{
private:
  string typ;            // Biltyp, t.ex. "Volvo"
  daeck hf, vf, hb, vb;  // Fyra st hjul med däck.
 
public:
  bil();               // Konstruktorer, 4st.
  bil( string nytyp );
  bil( double nyhf, double nyvf,  
       double nyhb, double nyvb );
  bil( daeck inhf, daeck invf,
       daeck inhb, daeck invb );
  string haemta_typ();        // Vilken typ?
  int gaspaadrag( double d ); // Gör gaspådrag.
  void skriv();               // Skriv bilinfo.
};

 

Då kan konstruktorerna se ut så här:

bil::bil()
   : typ("Ingen") 
{
  // Här kommer standardkonstruktorn för daeck
  // att anropas automatiskt fyra ggr (hf,vf,hb,vb).
}
 
bil::bil( string nytyp )
   : typ( nytyp )
{}
 
bil::bil( double nyhf, double nyvf, 
          double nyhb, double nyvb )
  : hf(nyhf), vf(nyvf),
    hb(nyhb), vb(nyvb), typ("--")
{}

Vi anväder initieringslistan och passar på att använda daeck-klassens överlagrade konstruktor när vi sätter värdet på däckobjekten i bilobjektet.

Viktigt om konstruktorer

·   om klass A innehåller objekt ur klass B, så anropas automatiskt klass B:s standardkonstruktor (default-konstruktor) om man ej anger ngt annat.

·   att alla klasser skall ha standardkonstruktorer, annars får man kompileringsfel. I vår diskussion om att vi redan stött på aggregat, då vi använt string-objekt som attribut, så nämnde vi att vi ej behövt tänka extra för att ha den med. Men detta bygger helt på att klassen string har konstruktor, destruktor mm redan inbyggt, som vi ej kunnat se.

Kopieringskonstruktorn

Man kan ibland se aggregat-klasser som har en överlagrad konstruktor av följande typ:

bil( daeck inhf, daeck invf,
     daeck inhb, daeck invb );

dvs man skickar in däckobjekt istället för flyttal som vi hade ovan. Implementationen kan göras på flera olika sätt t.ex.:

bil::bil( daeck inhf, daeck invf,
          daeck inhb, daeck invb )
{
  hf = inhf; vf = invf;   // tilldelning av däck
  hb = inhb; vb = invb;
}

eller

bil::bil( daeck inhf, daeck invf,
          daeck inhb, daeck invb )
: hf(inhf), vf(invf), hb(inhb), vb(invb)
{}

Det senare innebär att en inbyggd kopieringskonstruktor i klassen daeck sköter initieringen av objekten hf, vf, hb, vb.

Kopieringskonstruktorn tar alltså en inparameter av samma typ som objektet själv och skapar en kopa av det. Kopieringskonstruktorn arbetar "bakom kulisserna" vid andra tillfällen också, t.ex. då man skickar objekt som värdeparameter till en metod.

Aggregat - hur arbeta?

Huvudprogram

Vi fortsätter med bilexemplet och skall se hur objekten samverkar. Ett testhuvudprogram som har två bilar och gör ett gaspådrag med dessa. Även om en bil har fyra däck, så antar vi i detta program att däcken i varje däckpar (höger/vänster) slits lika mycket, men det kan slitas mer i bak om det är en bakhjulsdriven bil t.ex. Huvudprogrammet kan se ut så här:

int main()
{
  bil Volvo( "Volvo" );
  bil Saab( "Saab" );
 
  Volvo.skriv(); cout << endl;
  Saab.skriv();  cout << endl;
 
  int antVolvo, antSaab;
 
  antVolvo = Volvo.gaspaadrag( 4.0 );
  antSaab  = Saab.gaspaadrag( 4.0 );
 
  cout << "Antal slutslitna Volvodäck: " 
       << antVolvo << endl;
  Volvo.skriv(); cout << endl;
  cout << "Antal slutslitna Saabdäck: " 
       << antSaab << endl;
  Saab.skriv(); cout << endl;
 
  return 0;
}

Talet 4.0 som skickas som inparameter till gaspaadrag är en "däckslitagekonstant" och anger hur snabbt gummit slits. Man kan tänka sig att denna beror på hur aggresivt föraren gasar. Körningsresultat:

Bil av typen = Volvo     har  däck = 20.0 däck = 20.0
Bil av typen = Saab      har  däck = 20.0 däck = 20.0
Antal slutslitna Volvodäck: 0
Bil av typen = Volvo     har  däck = 8.0 däck = 6.0
Antal slutslitna Saabdäck: 0
Bil av typen = Saab      har  däck = 8.0 däck = 6.0

Inget särskilt roligt program precis. Men men hjälp av slumptal kan det bli lite roligare, så att man istället för "4.0" ger ett slumptal mellan 0 och 4.0 som inparameter till metoden som gör ett gaspådrag. Då kommer bilarnas däck slitas lite slumpmässigt mycket varje gång. Man kan leka biltävling och göra gaspådrag om och om igen tills en av bilerna har slut på gummi - då har den andra vunnit. Mer om detta i tillhörande laboration!

Bilklassens metoder

Gaspådrag

Studera metoden gaspaadrag:

int bil::gaspaadrag( double d );

där däckslitagekonstanten d anger hur aggresivt man gasar - en stort tal innebär mycket slitage. Implementationen av metoden skulle kunna se ut så här:

int bil::gaspaadrag( double d )
{
  // Däcken slits vid varje gaspådrag som
  // föraren gör.
  // Parameter: d = däckslitagekonstant,
  // som beror på hur aggresivt föraren gasar.
  // Rekommendation tal i intervallet 2-5.
  // Antag att däcken i varje däckpar 
  // (höger/vänster) slits lika mycket.
 
  // Antag vidare att vi har en bakhjulsdriven bil.
  // Dvs det sliter mer på bakhjulen än framhjulen
  // då man gasar.
  // Konstanterna "framslit" och "bakslit" har 
  // beräknats från mätningar vid rallytävlingar.
 
  const double framslit = 3.0;   
  const double bakslit  = 3.5;   // Ty bakhjulsdriven.
 
  double fram, bak;
  fram = d*framslit;  // Enligt rallyberäkningar.
  bak  = d*bakslit;
 
  // Kom ihåg: daeck-klassens metod "slitage"
  // returnerar 1 om däcket har slut på gummi,
  // annars returneras 0.
 
  int sum; // sum = antal däck som är
           // helt utslitna på bilen
 
  sum =  vf.slitage(fram) + hf.slitage(fram) +
         vb.slitage(bak) +  hb.slitage(bak);
 
  return sum;
}

För att kunna göra någor med bilens däck, t.ex. ett gaspådrag, så måste man alltså använda metoder i daeck-klassen på däckattributen vf, hf, vb, hb i bil-klassen. Här använder vi slitage-metoden.

Utskriftsmetod

Vi visar även bilens utskriftsmetod, skriv, som utnyttjar daeck-klassens utskriftsmetod.

void bil::skriv()
 {
   // Får lite snyggare utskrifter med "setf"...
   cout.setf( ios::fixed ); 
 
   cout << "Bil av typen = " << typ << "\t har ";
   hf.skriv(); // Skriver ut ena däcket i fram
   hb.skriv(); // Skriver ut ena däcket i bak
}

 

Exempel

Länkar till filer:

Att tänka på

När man arbetar med aggregatklasser, så gäller följande:

·   För att göra något med bilens däck, så måste man använda metoder i klassen daeck eftersom mönsterdjupet, djup, är private-deklarerad.

·   Jobbar ofta iterativt. Om man vid konstruktion av metod i klassen bil upptäcker att klassen daeck skulle behöva fler metoder, då så skapar man dem och återvänder sedan till bil-klassen.

·   Undvik kompilerings- och logiska fel: Jobba alltid lite i taget. Lägg till metod efter metod systematiskt - inte allt på en gång!!!. Testa efter hand.

·   Öka på med flera attribut efter hand (bensintank, x-position,...). Testa efter hand!

·   Ibland enklast att skriva main först, för att se vilka objekt och metoder man önskar ha. Sedan deklarerar man klasser och implementerar deras metoder efteråt.

Aggregat - Arrayer av objekt som attribut

Det är ganska vanligt med klasser som innehåller attribut som är arrayer av objekt. Vi exemplifierar med att bygga vidare på cirkel-klassen, som användes i avsnittet "Klasser introduktion" och skapar en klass bollhav.

Skriv ett program som simulerar ett bollhav på hamburgerrestaurang (ett sådant som är fullt av små färgglada plastbollar och barn). Bollarna krockar med varandra och med barnen och studsar ibland till och med ut ur inhägnaden.

Fortfarande har vi en förenklad modell, dvs vi tar bort barnen helt, men använder slumptal för att få bollarna att flytta sig som om det hoppade barn i bollhavet. Bollhavet är tvådimensionellt, dvs området där bollarna befinner sig är enhetskvadraten: 0≤x≤ 1, 0≤y≤1. Bollarna är små cirklar.

Anmärkning:
Meningen med exemplet är att visa:

·   hur man kan återanvända klasser

·   hur objekt i arrayer hanteras

·   hur objekt i samma klass kan samverka med varandra

Vi är medvetna om att det ej är ett "verklighetstroget simuleringsprogram med många finesser".

Huvudprogram och körning

Studera nedanstående huvudprogram för testning av en enkel bollhavssimulering:

int main() 
{
  // Initiera "slumptalsgeneratorn".
 
  long froe = long( time(NULL) );       //(1)
  srand48( froe );                      //(2)
  
  // Deklarera bollhav och testa lite:
 
  bollhav MacDonalds( 10, 0.15 );       //(3)  
  MacDonalds.skrivut();                 //(4)
  MacDonalds.flytta();                  //(5)
  MacDonalds.krock();                   //(6)
  int antutan = MacDonalds.utanfoer();  //(7)  
 
  cout << "Dumma ungar, nu är " << antutan 
       << " st bollar utanför bollhavsområdet!!!" 
       << endl;
  MacDonalds.skrivut();                 //(8) 
 
 return 0;
}

Förklaringar till koden i main:

·   (1): Vi använder datorns klocka för att skapa ett s.k. frö till slumptalsgeneratorn. Detta görs för att få lite olika resultat. I programmeringssammanhang gäller att slumptalsföljden alltid blir samma om samma frö ges, vilket inte är så kul.

·   (2): srand48 startar slumtalsgeneratorn. På PC/Borland heter den bara srand, se kursavsnittet "Mer om funktioner".

·   (3): Överlagrad konstruktor för bollhav används för att slumpmässigt placera ut bollar inne i enhetskvadraten.

·   (4): Testutskrift av all info, dvs hur många bollar vi har samt deras placering.

·   (5): Antag att ett barn (osynligt) hoppar ned i bollhavet, så bollarna rubbas. Detta simuleras med hjälp av slumptal. Bollarna flyttar på sig lite i x- och y-led.

·   (6): Kolla om några bollar krockar med varandra. Skriv ut det i så fall.

·   (7): Räkna hur många bollar som hoppade ut ur bollhavet.

·   (8): Testutskrift igen.

Exempel
Körningsresultatet och C++-koden: visas:

Definition av klassen bollhav

Klassen bollhav kan definieras som nedan, genom att använda den färdiga cirkel-klassen:

const int MAX = 100; // Max 100 bollar.
 
class bollhav
{
private:
  cirkel bollar[MAX];    // bollar i bollhav
  int antal;             // antal bollar
 
public:
  bollhav(); // Standardkonstruktor.
             // Slumpa ut bollar i rummet.
  bollhav( int antboll, double radie ); 
  void flytta();    // Flytta slumpmässigt
  void skrivut();   // Skriv info om bollar
  int utanfoer();   // Antal bollar utanför?
  void krock();     // Krockar bollar?
};

 

Definition av metoderna i bollhav

Nedan visar vi några av metoderna, men länk till hela programmet finns angiven i gula rutan ovan.

När man arbetar med aggregatklasser som består av arrayattribut, så brukar det ofta bli en del loopar i metoderna, eftersom man vill gå igenom alla arrayobjekten.

Överlagrad konstruktor

Konstruktor som placerar ut bollar slumpmässigt. Överlagrad konstruktor i cirkel-klassen används för att skapa själva bollobjekten, som sedan tilldelas arrayen.

bollhav:: bollhav ( int antboll, double radie )
: antal(antboll)
{
  for ( int i = 0; i < antal; i++ )
    {
      bollar[i] = cirkel( drand48(), drand48(), radie );
    }
}

 

Metoden utanfoer

Metod som räknar antalet bollar som hamnat utanför bollhavet, dvs utanför enhetskvadraten.

int bollhav:: utanfoer()
{
  //Antag ingen boll utanför rutan [0,1]x[0,1] först.
 
  int utan = 0; 
 
  for ( int i = 0; i < antal; i++ )
    {
      if ( bollar[i].hamtax() < 0.0 ||
           bollar[i].hamtax() > 1.0 ||
           bollar[i].hamtay() < 0.0 ||
           bollar[i].hamtay() > 1.0 )
        utan++;
    }
  return utan;
}

För att kunna kolla om en cirkel (boll) är utanför så behöver vi komma åt mittpunkten för cirkeln. Eftersom (x,y) är private-deklarerade i cirkel-klassen, så måste vi använda dess selektorer hamtax, hamtay.

Metoden flytta

Metod som flyttar alla små bollar i bollhavet lite slumpmässigt i x- och y-led, dvs simulerar att barn hoppar i bollhavet.

x = x + dx,   dx = tecken1*radie*slumptal1,
y = y + dy,   dy = tecken2*radie*slumptal2,

där dx och dy är små tal och tecken1, tecken2 anger bollens riktning i x- och y-led, dvs positivt eller negativt hopp. För att få tecken, så tar vi ett slumptal igen och om det är mindre än 0.5, så blir det "minus", om det är större än eller lika med 0.5 blir det "plus". Hur stora avstånd bollarna kan hoppa beror även på deras radie.

void bollhav:: flytta()
{
  double dx, dy;
  int tecken1, tecken2;
  double radie;
 
  for ( int i = 0; i < antal; i++ )
    {
      // Åt vilket håll?
 
      if ( drand48() < 0.5 )
           tecken1 = -1;
      else 
           tecken1 = 1;
 
      if ( drand48() < 0.5 )
           tecken2 = -1;
      else 
           tecken2 = 1;
 
      // Radie hos boll nr "i":
      radie = bollar[i].R();
 
      // Hur mycket skall boll nr "i" flyttas?
 
      dx = drand48()*radie*tecken1;
      dy = drand48()*radie*tecken2;
 
      // Själva flyttningen:
      bollar[i].flytta( dx, dy );
    }
}

Som vi ser används cirkel-klassens metod flytta för vart och ett av objekten i arrayen bollar.

 

Klasser, aggregat - laboration

Bakgrund

I objektorienterad programmering så försöker man utnyttja redan färdiga klasser om sådana finns. I denna laboration skall du arbeta med bilar. Vi låter en bil karakteriseras av att den har en bensintank och fyra hjul med gummidäck (strunta i motorn, karossen, gaspedalen, bromsen, vikten,...etc), samt en position. Vi tänker oss att bilen åker endimensionellt, dvs längs med x-axeln. En bil kan åka så länge den har bensin och den sliter på sina gummidäck varje gång man gasar.

Förenklingar

Som du märker måste man ofta vid simuleringar införa förenklingar. Det är alltid lättare att börja med en enkel modell och när sedan den fungerar kan man utöka. En vanlig variant är att man simulerar i en eller två dimensioner istället för tre, som i verkliga livet!

Byggstenar att arbeta med

Du har följande klasser/funktioner till förfogande:

·   klassen daeck, som representerar ett bildäck och har mönsterdjup i gummi, djup som attribut.

·   klassen bensintank, som representerar en bensintank och har antal liter i tanken och tankens maximala volym som attribut.

·   klassen bil, som från början är lite ofullständig och enbart har attributen fyra däck och en typ (sträng, t.ex. "Volvo").

·   funktionen slump, som ger slumptal i intervallet 0 till X, där X är en double som ges som inparameter till funktionen.

Uppdelning av kod

Som vi redan nämnt är det bra att i C++ dela upp sina program i filer av olika slag: headerfiler för klassdefinitioner, metodimplementationsfiler och testhuvudprogram av olika slag. Dock vet vi att det kan vara bökigt att hantera, så i denna laboration ger vi er all kod i samma fil för varje uppgift.

Svar

Lösningsförslag till uppgifterna.

Uppgift 1

Kopiera filen
testadaeck_start.cpp
som enbart innehåller klassen daeck med tillhörande tomt testhuvudprogram. Fyll i kod i main så att två däck skapas och metoder i klassen testas på dessa. Skriv ut resultat då och då.

Uppgift 2

Kopiera filen
bil_0.cpp
som är samma vi gått igenom i webbsidorna för detta kursavsnitt om aggregat. Nu finns en bil som har en typ (t.ex. "Volvo") och fyra däck. Kompilera och testkör. Inte så roligt kanske..., men med hjälp av slumptal kan vi piffa upp det lite.

Funktionen slump

Inför nu den vanliga funktionen slump som ger ett slumptal mellan noll och tal. Observera att det ser annorlunda ut i Borland/PC.

double slump( double tal )
{
  // För att få tal i intervallet 0-tal på SUN:
  // return drand48()*tal;
  // För att få tal i intervallet 0-tal på PC:
  // return tal*rand()/double(RAND_MAX);
 
  return drand48()*tal;  //Vi kör på SUN nu
}

Deklarera funktionen överst i filen t.ex., dvs direkt före klassen daeck. Implementationen (koden ovan), kan t.ex. läggas sist i filen.

Skriv nytt huvudprogram, så att det blir en liten tävling mellan två bilar. Börja så här:

int main()
{
  // Starta slumptalsgeneratorn. 
  int froe = long( time( NULL ) );
  //  srand( froe );   // På PC
  srand48( froe );   // På Sun
 
  // Nu är slumptalsgeneratorn initierad
  // med ett frö, dvs nu kan funktionen
  // "slump" anropas t.ex. här i main för
  // att få slumptal.
 
  //....... C++-kod skall in här.....
 
  return 0;
}

Algoritm:

1.      Initiera slumptalsgeneratorn.

2.      Skapa två bilobjekt av typen "Volvo" och "Saab".

3.      Skriv info om bilobjekten.

4.      Repetera i en loop:

1.      Gör gaspådrag med Volvo. Låt däckslitagekonstanten (parameter till metoden gaspaadrag) vara ett slumptal mellan noll och 4.

2.      Gör gaspådrag med Saab, Låt däckslitagekonstanten vara ett slumptal mellan noll och 4.

3.      Skriv info om objekten.

4.      Gör en liten paus, dvs kräv att användaren trycker return efter varje gaspådrag. (Görs med ledtext och cin.get();).


Snurra på i loopen så länge som antal slita däck på båda bilarna var för sig är mindre än fyra. Om någon bil har alla sina däck nedslitna, så stannar loopen.

Uppgift 3

Antag att man vill simulera ett gaspådrag med en mer utvecklad bil. Vi lägger till några attribut. Man är intresserad av att veta t.ex:

·   hur långt bilen kommer

·   hur många liter bensin som går åt

·   hur många millimeter av gummit på däcken som slits bort

Ny klass bensintank

Definitionen för daeck är oförändrad och definitionen för den nya klassen bensintank ser ut så här:

class bensintank
{
private:
  double liter; // Antal liter bensin i tanken.
  double volym; // Max antal liter, dvs tankens "volym".
 
public:
  bensintank();
  bensintank( double lit ); // Full tank (volym=liter).
  bensintank( double lit, double vol );
  double haemta_liter();
  void foerbruka( double & lit ); // Bensinen minskar
                                  // om det går.
  void skriv();    
};

För att testa enbart bensintank, kopiera hem följande fil och kompilera/kör:
testabilbensintank.cpp

Uppdaterad definition av bil

Definitionen för bil ser nu ut så här:

const int framhjulsdriven = 1;
const int bakhjulsdriven = 0;
 
class bil
{
private:
  string typ;            // Biltyp, t.ex. "Volvo"
  int framhjuls;         // Framhjuls-bakhjulsdriven?
  bensintank tank;       // Bensintanken.
  daeck hf, vf, hb, vb;  // Fyra st hjul.
  double xpos;           // x-position, 0=startlinjen.
 
public:
  bil();                            // Konstruktorer.
  bil( string nytyp,  int nyfram );
  string haemta_typ();        // Vilken typ?
  int tom();                  // 1=tom tank, 0 annars. 
  double haemta_x ();         // Var är bilen?
  void gaspaadrag( double d, double b); // Gör gaspådrag.
  void skriv();                         // Skriv bilinfo.
};

Kopiera hem följande fil och testkör:
alla_start.cpp

Studera koden. Observera att programmet är körbart, men metoderna i bil-klassen är ej fullständiga och korrekta.

Slitage

Slitage av däck samt bensinåtgång vid varje gaspådrag är beroende dels av ett slumptal samt dels om bilen har framhjuls- eller bakhjulsdrift. Följande finns i metoden gaspaadrag nu:

void bil::gaspaadrag( double d, double b )
{
  // Däcken slits och bensin går åt 
  // vid en rivstart.
  // Parametrar: d = däckslitage,
  // b = bensinminskning.
  // Beroende på om bilen är framhjuls-
  // eller bakhjulsdriven, så slits
  // den på lite olika sätt...
  //
  // Dessutom, man kan köra på helslitna däck, 
  // men då går det åt mycket mer bensin!
  //
  // Antag att däcken i varje däckpar
  // (höger/vänster) slits lika mycket.
 
  // Diverse vetenskapligt framtagna konstanter...
  const double framslit = 2.0;
  const double bakslit = 2.5;
  const double framliter = 1.75;
  const double bakliter = 1.5;
  double fram, bak, lit;
 
  if ( framhjuls )
    {
      fram = d*framslit;
      bak  = d;
      lit  = b*framliter;
    }
  else
    {
      fram = d;
      bak  = d*bakslit;
      lit  = b*bakliter;
    }
 
  //*** Här saknas några
  // variabeldeklarationer/initialiseringar***
 
  // init=innan. 
  double hur_mycket_soppa = tank.haemta_liter();  
 
  if ( hur_mycket_soppa > 0 )
    {
      //***Här saknas det kod***
      // * bygg upp sum genom att slita på däcken.
      // * förbruka rätt mängd bensin.
      // * fixa nya x-positionen.
    }
}

där framhjuls är ett attribut som har heltalsvärdet 1 om bilen är framhjulsdriven, annars 0. Dvs variabelrna fram, bak, lit skall skickas in till metoderna slitage (i daeck-klassen) och foerbruka (i bensintank-klassen).

Slut på däckgummi

Om man kör slut på gummit (dvs om djup==0.0) så straffas man med att det går åt mer bensin. Man skulle kunna tänka sig att byta däck som alternativ, men det struntar vi i nu.
Formel för bensinåtgång vid ett gaspådrag:

soppaminus = ( 1 + sum )*lit;

där variabeln sum är antalet däck som har slut på gummit.

Hur långt åker bilen?

Den sträcka som bilen åker under ett gaspådrag uppfyller följande formel:

nya x-positionen = gamla x-positionen + antal liter bensin som gick åt + 10 gånger totala antalet mm gummi som slets bort.

OBS! Om tanken är tom, så skall inte x-positionen uppdateras.

Uppgift att göra

Fortsätt och bygg på koden för
alla_start.cpp

Skriv de metoder som saknas eller är ofullständiga i klassen bil. Kompilera och testkör!

Ett sätt att se om metoden gaspaadrag fungerar som den ska är att anropa den med "icke-slumptal" i huvudprogrammet, dvs byt

Volvo.gaspaadrag( slump(2.0), slump(20.0) );
Saab.gaspaadrag( slump(2.0), slump(20.0) );

mot

Volvo.gaspaadrag( 2.0, 20.0 );
Saab.gaspaadrag( 2.0, 20.0 );

Ändra, kompilera om och testkör. Verkar det fungera som du vill?

Uppgift 4

Bygg vidare på den lösning du fick i uppgift 3, eller kopiera ett program här:
alla_svar.cpp

Komplettera programmet med ett nytt huvudprogram, så att det utför en tävling mellan de två bilarna. Tävlingen innebär en serie gaspådrag fram tills dess att båda får slut på bensin. Skriv ut information om den bil som kom längst.

Uppgift 5

Bygg vidare på den lösning du fick i uppgift 3, eller kopiera ett program här:
alla_svar.cpp

Modifiera huvudprogrammet så att det hanterar en array av ca 10 st bilar som skall tävla. Kör en serie av gaspådrag för varje bil och sluta tävlingen då alla bilar har slut på bensin. Skriv ut information om den som kom längst.

Uppgift 6

Vill du se hur konstruktorer anropas när man arbetar med aggregatklasser, lägg i så fall in testutskrifter i dem, kompilera och kör igen.


Lösningsförslag

1.      Uppgift 1: (startfil: testadaeck_start.cpp)
testadaeck_svar.cpp

2.      Uppgift 2: (startfil: bil_0.cpp)
bil_1.cpp

3.      Uppgift 3: (startfil: alla_start.cpp)
alla_svar.cpp

4.      Uppgift 4: (startfil: alla_svar.cpp)
alla_taevling.cpp

5.      Uppgift 5: (startfil: alla_svar.cpp)
alla_arraymain.cpp

Aggregat - Sammanfattning

·   Ett aggregat är en klass som har instanser av andra klasser som attribut:

en villa kan t.ex. ses som ett aggregat av kök, badrum, vardagsrum och sovrum (om man ser villan som ett ställe där man bor, en byggmästare skulle kanske se villan som ett aggregat av en massa byggmaterial och halvfabrikat).

·   Testmening: "består av", innehåller", "har".

·   Aggregat implementeras oftast i C++ som "vanliga" variabler, inte med pekare.

·   Viktigt att komma ihåg att alltid skapa standardkonstruktorer till sina klasser, så kan de återanvändas i aggregatform av andra klasser.

Studieplan: Klasser - pekare

Mål: Bli mer bekant med pekare och se hur de kan användas i program med klasser. Hur en klass hanterar dynamisk minnesallokering och hur man dynamiskt skapar instanser av klasser.
Läsanvisning: 10.4, vissa exempel i kapitel 11 (Arv) i "Programmeringsteknik med C++".


Studieplan

·   Läs igenom materialet på kommande webbsidor + tillhörande programexempel, som kan laddas hem som C++-filer för egna testningar.

·   Läs i kompendiet. Vid behov, ladda hem filer och testa själv.

·   Utför laborationen.

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen.


Översikt

Pekare kan användas på många olika sätt. Vi tar ej upp alla varianter här, men visar t.ex. association mellan klasser, dynamiskt instansierade objekt och dynamiska arrayer av objekt.

Klasser & pekare - pekare till objekt

Pekare till objekt

Lite om minne och pekare

Att reservera minne för en variabel kallas att allokera minnesutrymme. När inte minnet behövs längre så frisläpps det, eller deallokeras (även kallat avallokeras). Variabler som deklareras på vanligt sätt allokerar och frisläpper minne automatiskt. Kompilatorn vet exakt när detta sker (t.ex. dör ju variabler i slutet av ett "begin-end-block", då "}" dyker upp).

I många fall vet man före programmeringen hur många variabler man behöver och t.ex. hur långa arrayer man behöver, men ibland behövs mer flexibilitet, t.ex. då man har dynamiska arrayer som skapas under körningens gång. Det är då pekare kommer in i processen. Programmeraren sköter allokering (med new) och frisläppning (med delete) av minne och använder pekare för att peka på det minnesutrymme som är allokerat.

Kom ihåg:
"Att göra delete på en pekare" betyder att lämna tillbaka minnet som pekaren pekar på till minneshanteraren, inte att ta bort själva pekaren (pekarvariabeln)! Ha för vana att alltid sätta pekaren till NULL (=0) när du gör delete, så den har ett vettigt värde.

Man kan även använda pekare för att peka på redan deklarerade variabler, genom att använda &-operatorn som ger adressen i minnet till given variabel. Då behöver man inte allokera och frisläppa minne. Du kommer att få se exempel där vi använder pekare både till statiskt allokerat minne och dynamiskt allokerat minne.

Hur använda metoder till objekt en pekare pekar på?

Att använda pekare för objekt sker på samma sätt som pekare till double eller int t.ex.

Att utföra en operation via en objektpekare utförs normalt genom notationen:

identifierare->medlemsfunktionsanrop(...);

som är ett alternativt skrivsätt till:

(*identifierare).medlemsfunktionsanrop(...);

 

Exempel med villor

Vi visar pekare till objekt med ett exempel. Antag t.ex. att en mäklarfirma vill ha ett program som håller ordning på villor som är till salu. Vi startar med klassen Villa:

class Villa 
{
private:
  string adress;   // Villans adress.
  int pris;        // Villans pris.
 
public:
  Villa();
  Villa( string inAdress, int inPris );
  void skriv();
};

 

Pekare till statiska objekt

Vi leker lite med objektpekare i ett huvudprogram som ej har dynamisk minnesallokering. (Programmet gör inte så mycket vettigt - men i någon ände måste vi ju börja :-)

int main()
{
  // Två objekt finns inne på fastighetsförmedlingen:
 
  Villa villa1("Ekvägen 123", 1000000);  
  Villa villa2("Alvägen 12", 1200000);   
 
  // Vi låter pekare peka ut dem.
 
  Villa *v1, *v2;
  v1 = &villa1;  // Pekare v1 pekar nu på minnesutrymme 
  v2 = &villa2;  // där villa1 är. P.s.s. med v2 
                 // och villa2.
 
  cout << "---Skriv info om villorna" << endl;
 
  v1->skriv(); cout << endl;
  v2->skriv(); cout << endl;
 
  // Sälj alla villor för skojs skull...
  // Vi tar bort själva pekandet, men egentligen
  // ligger villorna kvar i minnet, eftersom vi
  // deklarerade dem statiskt som "villa1"
  // och "villa2".
 
  v1 = 0;
  v2 = 0;
 
  return 0;
}

 

Hela programmet (inklusive alla metoder) samt körexempel:

Exempel

Pekare till dynamiska objekt

Samma klass och exempel som ovan, men med ett nytt huvudprogram, där vi allokerar minne för nya villor då de behövs och avallokerar då de säljs.

int main()
{
  Villa *v1, *v2;
 
  // Två objekt kommer in till fastighets-
  // förmedlingen:
 
  v1 = new Villa("Ekvägen 123", 1000000);  
  v2 = new Villa("Alvägen 12", 1200000);   
 
  // Vi säljer genast villa "v1".
 
  delete v1;   // Lämna tillbaka det minne v1 
  v1 = 0;      // pekar på. Sätt pekaren till NULL.
 
  // Skriv info om våra objekt om det går, dvs
  // om pekarna pekar på något (dvs är nollskilda).
 
  cout << "---Skriv info om villorna" << endl;
 
  if ( v1 != 0 )
    {
      v1->skriv(); cout << endl;
    }
  else
    cout << "Villan fanns ej!" << endl;
 
  if ( v2 != 0 )
    {
      v2->skriv(); cout << endl;
    }
  else
    cout << "Villan fanns ej!" << endl;
 
  // Sälj alla villor för skojs skull...
  // utan att kolla om de finns eller ej.
 
  delete v1;
  delete v2;  // Vi får göra delete på 
              // noll-pekare, men inget händer.
  
  // Ha för vana sätta pekare till NULL 
  // efter delete. I allafall om programmet
  // är större; nu slutar det ju.
 
  v1 = 0;
  v2 = 0;
 
  return 0;
}

 

Hela programmet samt körexempel:

Exempel

Mer med dynamik...

Vi fortsätter med samma klass och samma metoder, men skriver ytterligare ett huvudprogram för att visa hur konstruktorer kan användas. Dock har vi gjort en dumhet i programmet...., gissa var!

int main()
{
  // Användning av standardkonstruktorn för
  // att skapa minne dynamiskt.
 
  Villa *v1 = new Villa, *v2 = new Villa;
 
  // Stoppa in värden i villorna....
 
  (*v1) = Villa("Ekvägen 123", 1000000);  
  (*v2) = Villa("Alvägen 12", 1200000);   
 
  cout << "---Skriv 1:" << endl;
  v1->skriv(); cout << endl;
  v2->skriv(); cout << endl;
 
  // Ändra lite för skojs skull...
 
  v2 = v1;  
 
  cout << "---Skriv 2:" << endl;
  v1->skriv(); cout << endl;
  v2->skriv(); cout << endl;
 
  delete v1;
  delete v2;
 
  return 0;
}

 

Det är mycket lätt hänt att man slarvar med minneshanteringen då man som programmerare skall sköta allokering och frisläppning. Felet i koden ovan är tilldelningsraden:

v2 = v1;

 

Här blir det minnesläckage, eftersom vi plötsligt bara glömmer bort att v1 pekade på dynamiskt allokerat minne, som vi aldrig tog bort. Villan på Ekvägen 23 ligger och skräpar i onödan i minnet och ingen kan någonsin hitta dit nu. (Jobbigt för mäklarfirman kan man tycka!) Så här skall det se ut:

delete v1; // Ta bort det v1 pekar på (Ekvägen).
v1 = v2;   // Låt v1 peka på samma som v2 (Alvägen).

 

Liten summering om pekare och klasser

Vad är vad?

Villa *p; // Pekaren p kan senare peka på Villaobjekt,
          // men just nu har p odefinierat värde.
Villa *q; // q kan senare peka på Villaobjekt,
          // men nu har q odefinierat värde.
 
q = new Villa;    // Nu får q ett riktigt värde och
                  // pekar på ett Villaobjekt.
                  // Detta objekt har det värde som
                  // standardkonstruktorn ger den.
                  // Värdet är odefinierat om
                  // ej standardkonstruktor finns.
 
*q = Villa("Ekvägen 123", 1000000);
 
// Nu pekar q på ett Villa objekt med värden:
// "Ekvägen 123", 1000000.
 
*p = Villa("Alvägen 12", 1200000);  
 
// Men ovanstående ger exekveringsfel,
// ty inget minne finns för det
// vi vill p skall peka på. 
 
Villa *r = 0;
 
// r kan användas till att
// peka på Villaobjekt, men nu
// sätts den till "nollpekaren" (NULL).

 

NULL-pekaren är användbar

NULL-pekare är bra, dvs även om pekaren inte pekar på något, så har den ett definierat värde. Tex kan man sedan testa på detta värde i if- och while-satser eller man skriver ut information om objektet man tror pekaren pekar på. Exempel:

if ( p != 0 )
  {
    cout << "Pekaren pekar på ";
    p->skriv();
  }
else
  cout << "Pekaren är NULLpekare, pekar inte på ngt."
       << endl;
...
 
if ( p )
  {
    // Vet utrymme finns allokerat!
 
    *p = Villa("Alvägen 12", 1200000);  
  }
else  // Null-pekare
  {
    // Fixa utrymme själv, eftersom vi har NULL-pekare.
 
    p = new Villa;                    
    *p = Villa("Alvägen 12", 1200000);
  }

 

Som du ser så kan man skriva if( p ) lika väl som if ( p != 0 ).

Arrayer av pekare till objekt

Naturligtvis kan man också ha arrayer av pekare till objekt av klasser. Antag att mäklarfirman vill ha ett program som håller ordning på villor som är till salu och att programmet kan hantera max 100 st villor.

Klass- och metoddefinition som ovan, dvs klassen Villa. Nu kan huvudprogrammet se ut så här:

int main()
{
  const int MAX=100;
 
  Villa *villor[MAX];   // Array av pekare till 
                        // många Villa-objekt.
 
  for ( int i = 0; i < MAX; i++ ) 
    {
      villor[i] = 0;   // Bra att nollställa 
    }                  // pekararrayen.
 
  int antal;         // Antal villor idag.
 
  do                 // Kontroll på inläsningen
    {
      cout << "Hur många villor?" << endl;
      cin >> antal;
    } while( antal <= 0 || antal > MAX);
 
  // Skapa lite villor, dvs simulering av att
  // objekt kommer in till försäljning.
 
  villor[0] = new Villa("Ekvägen 123", 1000000); 
  villor[1] = new Villa("Alvägen 12", 1200000); 
  villor[antal-1] = new Villa("Hävelvägen 7", 130000);
 
  // Bums säljer vi Ekvägen.
  cout << "---Nu säljer vi första villan" << endl;
 
  delete villor[0];     
  villor[0] = 0;     
 
  // Skriv ut de villor som finns kvar till
  // försäljning.
 
  for ( int i = 0; i < antal; i++ ) 
    {
      if ( villor[i] != 0 ) 
           {
             cout << "---Objekt nr " << i+1 << " ";
             villor[i]->skriv(); cout << endl;
           }
    }
  
  // En konkurrerande mäklarfirma
  // köper upp alla villaobjekt!
 
  for ( int i = 0; i < antal; i++ ) 
    {
      // Behöver ej kolla vilka pekare 
      // som är NULL. Man får göra delete 
      // på noll-pekare, fast inget händer...,
      // men det förenklar programmeringen.
 
      delete villor[i]; 
    }
 
  return 0;
}

 

Hela programmet samt körexempel:

Exempel

Klasser & pekare: listor

Vad är en lista?

En lista är en sekventiell struktur oftast av likadana saker. Listor definieras i C++ ofta med hjälp av klasser, men representationen (dvs vilka dataattribut klassen har) kan ske på olika sätt:

·   Indexerad lista: Varje element åtkoms med ett index.

o  Statisk array. Längden bestäms under kompileringen.

o  Dynamisk, tänjbar array. Kan växa och krympa under exekveringen.

 

·   Länkad lista: Varje element åtkoms med hjälp av pekare från t ex föregående element. Kan växa och krympa under exekveringen. Listan byggs upp av objekt, som pekar på varandra. Ett av klassens attribut är alltså pekare till objekt i just denna klass.

Vad bör en enkel lista klara av?

T.ex. kan nedanstående vara vettigt för en lista:

·   Tala om sin aktuella längd, dvs antalet listelement.

·   Tala om sin maximala kapacitet, dvs max antal element som listan kan ha.

·   Ta bort alla element, dvs göra sig själv till tom lista.

·   Ta bort ett element ur listan.

·   Lägga till ett nytt element, t ex sist.

·   Indikera att ett element redan finns i listan eller ej.

·   Indikera om listan är tom eller ej.

Olika sorters listor

·   Ordnad lista eller sorterad lista är en lista i vilken elementen ligger sorterade i stigande eller fallande ordning.

Ex. Telefonkatalogen, olika ``kartotek''.

·   Stack: LIFO-struktur (Last In First Out), dvs den som kommer sist blir behandlad först.

Ex. Olika former av travar, pappershögar och liknande.

·   Kö: FIFO-struktur (First in First Out), dvs den som kommer först blir behandlad först.

Ex. En lång rad konsumenter som hyser en gemensam längtan att träffa producenten av en resurs.

Listklass med dynamisk, tänjbar array

En tänjbar array upptar alltid precis så mycket minne som behövs. Den kan växa eller krympa om t.ex.:

·   användaren skickar ett meddelande om detta. Meddelandet innehåller nya längden.

·   den tilldelas en annan tänjbar array. Dess nya längd blir längden på det som tilldelades.

·   man adderar eller tar bort element.

Vi skall studera klassen ExtArray (extensible array) lite närmare nu:

const int blank = 0;
 
class ExtArray
{
private:
  int *array;
  int bufSize;
 
public:
  ExtArray();                     // standardkonstr. 
  ExtArray( int siz, int val );   // överlagrad konstr.
  ~ExtArray();                                // destruktor             
  ExtArray( const ExtArray & a ); // kopieringskonstr.
  
  void change(int i, int num);    // array[i]=num
  int size();                     // arraylängd
  void resize( int n );           // gör större/mindre array
  void assign( const ExtArray & a ); // tilldelning
  void write();                   // skriv info
  void addToEnd( int newItem );   // lägg newItem sist i array
};

Vi är medvetna om att denna listklass är lite tråkig, dvs arrayen innehåller enbart heltal här. Man går tillväga på exakt samma sätt när man har en dynamisk array av objekt som attribut i en klass.

Vi bör först veta mer om C++ och pekare....

Tilldelningsoperator och kopieringskonstruktor

Innan vi kan börja med att diskutera klasser som allokerar minne dynamiskt inne i metoder och konstruktorer, så skall vi se på några inbyggda klurigheter i C++, som man måste känna till.

Tilldelningar

Det finns en inbyggd tilldelningsoperator som gör att det fungerar utmärkt att tilldela objekt direkt med ett annat, dvs varianten

objekt1 = objekt2;

innebär att datavärde för datavärde (attributen) kopieras över från objekt2 till objekt1.

Då motsvarande klass inte har pekarattribut som krävt att man skapat dynamiskt minne med new i metoder eller konstruktorer.

Om vi har pekare som attribut, som den dynamiska arrayen i klassen ExtArray, så blir det lite annorlunda vid tilldelning av objekt.
Exempel:

ExtArray objekt1, objekt2;
.....
objekt1 = objekt2;

Detta innebär att varje attribut kopieras för sig, men eftersom ett attribut är en pekare (array) så menas det att även pekarna kopieras direkt! Dvs pekaren i objekt1 och pekaren i objekt2 pekar ut samma data (samma minnesutrymme, dvs det som objekt redan pekade på). Det dumma är att om man senare ändrar i objekt2:s array, så sker samma ändring i objekt1:s array, fast man troligen inte ville det!

Detta kallas grund kopiering (ej fullständig, ickedjup, attributkopiering, shallow copy). Se övre delen i figuren.

bild

Botemedel: Man skriver en egen tilldelningsfunktion, t.ex.

void ExtArray:: assign( const ExtArray & a );

dvs tilldelningen:

objekt1.assign( objekt2 );

fungerar då rätt om man implementerar assign så att den skapar en helt egen kopia av arrayen genom att kopiera element för element, s.k. djup kopiering (fullständig, deep copy).

Om &-tecknet glöms så måste en egen kopieringskonstruktor finnas, annars blir det fel i alla fall... (se nedan). Deklaration med const medför att objekt2 ej kan eller får ändras (=förstöras) inne i metoden.

Alternativt botemedel: Skriv en egen överlagrad tilldelningsoperator så att

objekt1 = objekt2;


innebär djup kopiering. En sådan deklareras i stil med det här:

const ExtArray & ExtArray:: operator= ( const ExtArray & a );

:-|
Inte helt trivialt med C++....
:-O

Mer om egna operatorer kan du läsa i fortsättningskursen i C++; i TDB2 ingår de ej.

Kopieringar

Det finns en inbyggd konstruktor som heter kopieringskonstruktorn i C++. Denna används automatiskt vid vissa tillfällen fast man inte tänker på det. Allt fungerar bra så länge som man inte har pekare bland attributen och har gjort new i konstruktorer eller metoder.

Kopieringskonstruktorn anropas

·   då man initierar objektvariabler

·   vid ickereferensparameteröverföring till funktioner

·   när man returnerar objektvärden från funktioner, dvs då returdatatypen är en klass.

Den inbyggda kopieringskonstruktorn gör, prcis som den inbyggda tilldelningsoperatorn, grund kopiering.

Botemedel: Man skriver en egen kopieringskonstruktor som kopierar elementvis (dvs djupt). Konstruktor med följande interface:

ExtArray( const ExtArray & a );

medför följade vid skapande av objekt:

int main()
{
...
  ExtArray arr1;           // default.konstr.
  ExtArray arr2( 2, -99 ); //överlagrad.
  ExtArray arr3(arr2);     // kopierings.konstr.
  ExtArray arr4 = arr2;    // kopierings.konstr.

 

Summering

Om man har pekare som attribut och gör new inne i metoder eller konstruktorer, så bör man alltså ha en egendefinierad destruktor, som gör delete, samt en egendefinierad kopieringskonstruktor och tilldelningsoperator/metod, som gör djup kopiering (förutsatt att det var det man ville ha..., men så är det oftast)

Implementation av ExtArray

Implementation av konstruktorer, destruktor och metoder för klassen ExtArray.

Konstruktorer och destruktorn

Standardkonstruktor

Sätter attribut till noll. Man kan låta initieringslistan sätta även pekaren till noll om man hellre vill det.

ExtArray:: ExtArray()
  : bufSize(0)
{
  array = 0; //NULLpointer
  cout << "Hello default " << bufSize << endl;
}

 

Överlagrad konstruktor

Här sätter vi storleken på arrayen till siz och låter alla element få värdet val, efter det att önskat mycket minne allokerats.

ExtArray:: ExtArray( int siz, int val )
  : bufSize(siz)
{
  if ( bufSize > 0 )
    {
      array = new int[bufSize];  // allokera minne
 
      for ( int i = 0; i < bufSize; i++ )
           array[i] = val;
    }
  else
    {
      array = 0; //NULL-pekare
    }
  cout << "Hello överlagrad " << bufSize << endl; } 

Kopieringskonstruktor

Glömmer vi denna, så sker alltså grund kopiering av pekare direkt. Parameterlistan för en kopieringskonstruktor ser alltid ut så här. Man skickar in den som skall kopieras, här a.

Konstruktorn måste ha referensparameter, för annars vill C++ kopiera parametern med hjälp av en kopieringskonstruktor (den inbyggda, som kopierar grunt), och då blir allt fel, eftersom det är motsvarande egna konstruktor vi just definierar! (rundgång på ngt vis...).

Parametern är konstantdeklarerad för att försäkra att även om parametern är referensparameter, så får den ej ändras inne i metoden. (Bra trix i allmänhet i C++, då man skickar stora objekt till metoder och funktioner och det skall gå snabbare än att skicka värdeparameter).

ExtArray:: ExtArray( const ExtArray & a )
  :  bufSize(a.bufSize)
{
  cout << "Hello kopiering: ny längd: " 
       << bufSize << endl;
 
  if ( bufSize > 0 )
    {
      array = new int[bufSize];  // Allokera nytt minne
 
      for ( int i = 0; i < bufSize; i++ )
           array[i] = a.array[i]; //Kopiera elementen
    }
  else
    array = 0; //NULL
}

 

Destruktor

ExtArray:: ~ExtArray()
{
  delete [] array;
}

 

Metoder

Metoden resize tar aktuella objektets array och gör om till en array av längd n, dvs förstorar (bygger ut med nya element på slutet av arrayen, med värde=0) eller förminskar (tar bort element på slutet i arrayen). Metoden använder en lokal temporär pekare, p för att bygga upp den nya arrayen.

void ExtArray:: resize( int n )
{
  int *p = 0;     // Lokal variabel.
  
  if ( n > 0 )    // Om ny storlek ej är noll
    {
      p = new int[n]; // Skapa array med n elem.
 
      int i; 
      for ( i = 0; i < n   // Fyll med element.
               && i < bufSize; i++ )
           {
             p[i] = array[i];
           }
      for ( ; i < n; i++ )      // Fyll resten med blanka .
           p[i] = blank;
    }
 
  delete [] array; // Avallokera minne.
  array = p;       // Sätt array till nya arrayen.
  bufSize = n;     // Sätt bufSize till nya storleken.
}

Metoden size returnerar aktuella objektets arraylängd.

int ExtArray:: size()
{
  return bufSize;
}  

Metoden write skriver ut information om attributen.

void ExtArray:: write()
{
  cout << "Antal element är " << bufSize;
  if ( bufSize > 0 )
    {
      cout << ":";
 
      for ( int i = 0; i < bufSize; i++ )
           cout << '\t' << array[i];
    }
  else
    cout << ".";
 
  cout << endl;
    
}

Metoden assign är istället för en egendefinierad tilldelningsoperator. Aktuella objektet skall få det värde som inparametern har. Det gamla som pekaren pekade på (dvs arrayen array) måste lämnas tillbaka till minneshanteraren med delete. Glömmer man detta, så tar minnet i datorn snart slut...

void ExtArray:: assign( const ExtArray  & a )
{
  // Tilldela: aktuella objektet = a.
 
  bufSize = a.bufSize;
  cout << "Hello assign " <<  bufSize << endl;
 
  delete [] array;           // Frigör minne.
  if ( bufSize > 0 )
    {
      array = new int[bufSize];  // Allokera nytt minne.
 
      for ( int i = 0; i < bufSize; i++ )
           {
             array[i] = a.array[i]; //Kopiera elementen
           }
    }
  else
    array = 0; //NULL
}

Metoden addToEnd lägger ett element sist i arrayen.

void ExtArray:: addToEnd( int newItem )
{
  // Öka på arrayen med ett element till.
  resize(bufSize+1); // Fixar även bufSize++;
 
  // Tilldela nu sista elementet, dvs plats bufSize-1:
  array[bufSize-1] = newItem;
}

Metoden change sätter ett värde, num på plats i i arrayen.

void ExtArray:: change(int i, int num)
{
  if ( i < bufSize )
    array[ i ] = num;
}

 

Exempel

Klicka på länken nedan för att få hela programfilen och körningsexempel för ExtArray. I laborationen skall man själv utöka programmet.

Exempel med ett tåg

Ett tåg består, som bekant, av ett lok (oftast är det bara ett i alla fall) och sedan en rad med vagnar efter varandra. Låt oss studera metoderna (+ konstruktorer och destruktor) i klassen Taag, som används för att simulera ett tåg. Till din hjälp har vi följande klassdefinitioner:

class Vagn
{
private:
  int idnr; // vagnens id-nummer. 1:a siffran anger vagntyp:
            // 1=klass1, 2=klass2, 3=gods, 4=restaurang
  int vikt; // vagnens vikt
 
public:
  Vagn();               // standardkonstruktor
  Vagn(int id, int v);  // överlagrad konstruktor
  int returnvikt();     // returnerar en vagns vikt
  int id();             // returnerar idnr
  void skriv();         // skriver ut vagndata
};
 
class Taag
{
private:
  string lokNamn; // Lokets namn
  int dragvikt;   // Maximal vikt som loket klarar av att dra
  Vagn *vagnar;   // Vagnar (dynamisk array).
  int antal;      // antalet vagnar
 
public:
  Taag();                  // standardkonstruktor
  Taag( string n, int dv); // överlagrad konstruktor. 
  ~Taag();                 // destruktor
  int kopplavagn(Vagn v);  // kopplar in en vagn i tåget 
  int totalvagnvikt();     // returnerar totala vagnsvikten
                           // i tåget
  void tabortvagn(int id );// ta bort vagn med idnr=id ur tåget. 
  void skriv();            // skriver ut Taagdata
};

Kommentarer:

·   Inparametrar till överlagrade konstruktorn i Taag-klassen är lokets namn och dragvikt, dvs hur mycket den orkar dra.

·   Vagnarna representeras av en s.k. dynamisk tänjbar array, som alltså växer/krymper då vagnar kopplas på/av.

·   När man skapar tåg, så har de aldrig några vagnar. För att bygga upp sitt tåg måste man använda metoden kopplavagn som bygger ut arrayen vagnar efter hand.

·   Man kopplar alltid in en ny vagn sist i tåget.

·   Då man kopplar bort en vagn, så måste man se till att koppla in de efterföljande vagnarna igen.

·   Utskriftsmetoden i Taag-klassen skall tala om lokets namn samt dragvikt och information om alla vagnarna.

·   Inga nya attribut och/eller metoder får läggas till.

Lösningsförslag finns här nedan.

Exempel

Kommentarer till lösningsförslaget:

Klassen Vagn är ganska rakt på utan klurigheter. För klassen tåg gäller att allokering av minne, dvs användandet av new, enbart sker i metoderna kopplavagn och tabortvagn Minnet lämnas tillbaka med delete i destruktorn (förstås!) samt i kopplavagn och tabortvagn.

De lurigaste metoderna är just dessa som hanterar förändring av den dynamiska arrayen vagnar. De andra, t.ex. totalvagnvikt, skriv hanterar vagnar som en vanlig array.

Klasser & pekare - association

Associationer, kort introduktion

Associationer är fasta samband mellan två objekt. Ett exempel är mellan personer och bilar t.ex. En person äger en bil. Det är knappast meningsfullt att anse att endera bilen eller personen är en del av det andra objektet så aggregat är det inte fråga om. Men även om man kan sälja bilen är sambandet mellan de två objekten fast, dvs vi har ett typfall av en association.

Aggregat brukar som vi sett implementeras helt enkelt genom att man låter det objekt som är en del av ett annat objekt vara ett attribut i det större objektet. Assocationer däremot implementeras oftast med pekare: på så sätt blir inget objekt en del av det andra och man kan bryta förhållandet (när man köper ny bil t.ex.).

Bilar och bilägare

Nedan visas klasserna Bil och Person. En person känner till att han/hon har en bil, men bilen vet ingenting om personer.

class Bil 
{
private:
  string typ;
 
public:
  Bil();
  Bil( string inTyp );
  void skriv();
};
 
class Person 
{
private: 
  string namn;
  Bil *bil_p;      // pekare till bilobjekt
 
public:
  Person ( string inNamn );
  Person ( string inNamn, Bil *b );
  void skriv();
  void koepBil( Bil *b );
  void saeljBil();
  void saeljPrivat( Person &annan );
};

 

Exempel på använding...

Huvudprogram med referenser till bilobjekt

int main()
{
  Bil b1("Lada"), b2("Rolls Royce"), b3("Volvo");
  Person p1("Kalle Anka", &b1);
  Person p2("Joakim von Anka", &b2);
 
  cout << "\n---Följande personer finns:" << endl;
  p1.skriv(); cout << endl;
  p2.skriv(); cout << endl;
 
  cout << "\n---Följande bilar finns:" << endl;
  b1.skriv(); cout << endl;
  b2.skriv(); cout << endl;
  b3.skriv(); cout << endl;
 
  cout << "\n---Kalle säljer sin bil till ngn." 
       << "\n---Kalle köper bil Volvo." << endl;
  p1.saeljBil();
  p1.koepBil( & b3 );
  p1.skriv(); cout << endl;
 
  cout << "\n---Joakim säljer sin bil till Kalle." 
       << endl;
  p2.saeljPrivat( p1 );
  p1.skriv(); cout << endl;
  p2.skriv(); cout << endl;
 
  return 0;
}

Hela koden och körningsexempel:

Exempel

Detta är ett exempel på en envägs en-till-en association, dvs envägs ty personen har en association till bilen men bilen har ingen association till personen, en-till-en för att det bara är en bil och en person inblandade i kopplingen.

Huvudprogram med pekare till bilobjekt

Ett huvudprogram som ger samma resultat som assoc0.cpp ovan, är detta med pekare till bilobjekt istället:

int main()
{
  Bil *b1_p, *b2_p, *b3_p;
  b1_p = new Bil("Lada"); 
  b2_p = new Bil("Rolls Royce");
  b3_p = new Bil("Volvo");
 
  Person p1("Kalle Anka", b1_p);
  Person p2("Joakim von Anka", b2_p);
 
  cout << "\n---Följande personer finns:" << endl;
  p1.skriv(); cout << endl;
  p2.skriv(); cout << endl;
 
  cout << "\n---Följande bilar finns:" << endl;
  b1_p->skriv(); cout << endl;
  b2_p->skriv(); cout << endl;
  b3_p->skriv(); cout << endl;
 
  cout << "\n---Kalle säljer sin bil till ngn." 
       << "\n---Kalle köper bil Volvo." << endl;
  p1.saeljBil();
  p1.koepBil( b3_p );
  p1.skriv(); cout << endl;
 
  cout << "\n---Joakim säljer sin bil till Kalle." 
       << endl;
  p2.saeljPrivat( p1 );
  p1.skriv(); cout << endl;
  p2.skriv(); cout << endl;
 
  delete b1_p;  // Gör egentligen automatiskt
  delete b2_p;  // innan programslut, men i allafall...
  delete b3_p;
  return 0;
}

 

Exempel

Vad gör den s.k.this-pekaren i C++?

Varje klassinstans har en pekare till sig själv! Denna självreferens heter i C++ this-pekaren.

Du kanske har frågat dig någon gång vad som händer egentligen då man skapar ett nytt objekt och hur det kommer sig att datorn automatiskt har koll på vilka attribut som finns i klasser då man anropar metoder.

För att förstå this-pekaren kan man tänka på vad som händer när ett objekt skapas:

·   Plats för data allokeras - olika för varje objekt

·   Metoderna däremot finns i en upplaga per klass och systemet använder den dolda this-pekaren för att veta vilket objekt som skall anropas. Man kan tänka sig att varje metod har en extra parameter som pekar ut vilket objekt som metoden skall operera på. C++ skickar alltså en dold pekare till metoderna, dvs en pekare till "aktuella objektet" - dvs det objekt som operationen skall utföras på.

Egentligen är

void Bil:: skriv()
{
  cout << typ;
}

samma sak som

void Bil:: skriv( Bil *this )
{
  cout << this->typ; 
}

eller om man så vill:

void Bil:: skriv( Bil *this )
{
  cout << (*this).typ; 
}

Som programmerare behöver vi oftast ej bry oss om this-pekaren, utan låta denna hantering ske i det fördolda i kompilatorn. Ibland finns det dock fall då man vill använda this-pekaren. T.ex. för att tala om för associerat objekt "vem jag är".

 

 

Klasser & pekare - laboration

Lösningar

Länk till lösningsförslag.

Uppgift 1: Association

Hund och matte

I filen
HundMatte0.cpp
finns klasserna Hund och Matte, där hundens ägare, dvs matten, "känner till" sin hund, men inte hunden sin matte.

Kod för huvudprogrammet:

int main()
{
  Matte matte1( "Matilda" );
  Hund *hund1_p;                    // Pekare till hund.
  hund1_p = new Hund( "Fido", 15 ); // Skapa Fido.
 
  cout << "Lite startinfo:" << endl;
  hund1_p->skriv(); cout << endl;
  matte1.skriv();   cout << endl;
 
  cout << "Matilda kopplar Fido" << endl;
  matte1.kopplaHund( hund1_p );
 
  cout << "Fido blir knubbigare" << endl;
  hund1_p->nyvikt( 25 );
 
  matte1.skriv();   cout << endl; 
  hund1_p->skriv(); cout << endl;
  return 0;
}

Körningsexempel:

Lite startinfo:
hund heter Fido och väger 15
(har ingen matte ännu!, fast matte kanste tror det!)
Matilda  har ingen hund...
Matilda kopplar Fido
Fido blir knubbigare
Matilda hund heter Fido och väger 25
(har ingen matte ännu!, fast matte kanste tror det!)
hund heter Fido och väger 25
(har ingen matte ännu!, fast matte kanste tror det!)

 

Komplettera filen

Du skall komplettera filen så att klassen Hund känner till Matte också. När en matte kopplar sin hund, så måste hunden koppla sin matte också (dvs referera till matte via pekare).

OBS! Var försiktig, annars kan du råka ut för rundgång, dvs om skriv -metoden i Matte använder skriv-metoden i Hund, så blir det "rundgång". Prioritera så att mattar är viktigare än hundar.

Så här kan körning se ut med samma huvudprogram som i originalfilen (dvs som ovan):

Lite startinfo:
hund heter Fido och väger 15
 är en herrelös hund, har ingen matte.
Matilda  har ingen hund...
Matilda kopplar Fido
Fido blir knubbigare
Matilda hund heter Fido och väger 25
 har matte = Matilda 
hund heter Fido och väger 25
 har matte = Matilda 

Anmärkning: I det här exemplet behöver vi inte kopieringskonstruktor och destruktor, eftersom vi inte gör new och delete i konstruktorer och metoder, utan bara i main. Dessutom, eftersom allokerat minne lämnas tillbaka då programmet slutar (innan return 0; i main) så kan vi strunta i att göra delete ...annars skall man vara noga med sådant "städjobb"!

Uppgift 2: Listor och listklasser

I filen
ExtArray0.cpp
finns kod för en dynamisk, tänjbar array. Lägg till följande extrametoder:

·   bool empty() som returnerar true om listan är tom, annars returneras false.

·   void append( ExtArray other ) som tar listan other och lägger in direkt efter sista elementet i aktuella listan.

·   Ändra i main

Uppgift 3: Listor och const...

I filen
ExtArray1.cpp
(lösningsförslag till uppgift 2) finns kod för en dynamisk tänjbar array.

Fler metoder

Utvidga klassen ExtArray så att följande metoder finns:

·   int last() som returnerar värdet på sista index.

·   int value( int index ) som returnerar värdet på plats index i listan.

·   Testa dem i huvudprogrammet.

Nya vanliga funktioner

Skriv följande "vanliga funktioner" (använd value, change, last):

·   int sum ( ExtArray & v ) som beräknar och returnerar summan av elementen i listan. Här har vi referensparameter för att slippa anrop till kopieringskonstruktorer.

·   void kvadrera( ExtArray & v ) som ändrar i listan så att varje element kvadreras.

·   Testa dem i huvudprogrammet.

Att fundera på

Fråga: Eftersom vi inte vill ändra på objektet v i sum-funktionen, så skulle vi kunna ha denna referensparameter const-deklarerad:

int sum ( const ExtArray & v );

Om vi nu antar att vi i sum-funktionen har följande

  for ( int i = 0; i <= v.last(); i++ )

Vad händer då?

Svar:
Bra att veta om const-deklarerade referensparametrar:

·   const utifrån sett:
const betyder att vi får anropa funktionen sum med ett konstant objekt om vi vill, men vi behöver det inte. Annars får man ju (som bekant) inte anropa med konstant som aktuell parameter om formella parametern är referensparameter.

·   const inifrån sett:
const betyder här att även om vi har referensparameter så garanteras att objektet v ej kommer att ändras. Bra för programmeraren att veta, för han/hon brukar ofta bara ha tillgång till själva "klass-interfacet", dvs lista av metodhuvuden.

Men... om vi garanterar att inget kan ändras i objektet och samtidigt använder metoder som last() så kan ju last ändra på v!!
Hur hindrar vi det?
Vi måste alltså const-deklarera metoden last så att användaren vet att metoden ej ändrar på objektet v, dvs vi måste nu ha följande:

int last() const;

Andra metoder i klassen som kan vara const-deklarerade är:

int size() const;
void write() const;
int last() const;
int empty() const;
int value() const;

 

Vi bryr oss ej om const så mycket här i TDB2-kursen, men du kan ju alltid testa litegrand.

Lösningsförslag

1.      Uppgift 1: (startfil: HundMatte0.cpp)
HundMatte1.cpp

2.      Uppgift 2: (startfil: ExtArray0.cpp)
ExtArray1.cpp

3.      Uppgift 3: (startfil: ExtArray1.cpp)
ExtArray2.cpp

Klasser & pekare - sammanfattning

Liten sammanfattning om klasser och pekare i C++.

Att komma ihåg

·   Alla klasser som har new i konstruktorn (eller i någon metod, t.ex. inläsningsmetoder) måste förses med en destruktor som återlämnar minnet.

·   Alla klasser som har new i konstruktorn (eller i någon metod, t.ex. inläsningsmetoder) bör förses med en egendefinierad kopieringskonstruktor, som gör s.k. djup kopiering. Annars kan det gå på tok vid objektkopiering, t.ex. om objekt hörande till klassen är värdeparameter till någon metod/funktion.

·   Varje klassinstans har en pekare till sig själv! Denna självreferens heter i C++ this-pekaren.

Studieplan: Klasser - arv

Mål:
Läsanvisning: kapitlen 9.1-9.8 i "C++ direkt", kapitel 11 i "Programmeringsteknik med C++".


Studieplan

·   Läs igenom materialet på kommande webbsidor + tillhörande programexempel, som kan laddas hem som C++-filer för egna testningar.

·   Läs i kompendiet. Vid behov, ladda hem filer och testa själv.

·   Utför laborationen.

Hjälp?

Behöver du hjälp eller kör fast, se de allmänna webbsidorna om "Hur arbeta?", "Hur kommunicera?" mm i första delen av kursen.


Översikt

Arv och polymorfi är en viktig del inom objektorienterad programmering. I denna kurs nosar vi bara lite på arv och visar bara några ganska uppenbara exempel, men i "verkligheten" användas arv mycket mer abstrakt inom OOP.

Med hjälp av arv kan man skapa nya klasser genom att utgå från redan existerande klasser och utöka dem med ytterligare attribut. S.k. är-relationer beskrivs med arv.

Med hjälp av arv kan man skapa klasser som är delvis lika men ändå olika, s.k. polymorfa klasser. För objekt i sådana klasser finns metoder med samma namn som logiskt utför liknande operationer, men resultatet blir olika beroende på var i arvsträdet de involverade objekten befinner sig. Det finns språkmekanismer i C++ som under körningen (runtime) känner av att rätt operation utförs på ett objekt, s.k. dynamisk bindning.

Nog låter det kryptiskt med arv! Men det är ganska roligt att programmera :-)

Arv - introduktion

För att exemplifiera arv i C++, så använder vi oss av klasserna Figur, Cirkel, Rektangel, Kvadrat. Vi kan anta att dessa klasser behövs i ett större sammanhang, t.ex. ett pedagogiskt grafikprogram om enkel geometri för barn. Vi håller oss i två rumsdimensioner.

... är en ...

Det som Cirkel och Rektangel har gemensamt är att de båda är "figurer" i tvådimensionella planet, xy-planet, och de har en position (x,y). Man tänker sig att de båda har ärvt egenskaperna från en moderklass, som vi här kallar Figur.

En Cirkel är en Figur och en Rektangel är en Figur. En Kvadrat är en Rektangel, men är också en Figur.

Hierarkier

Vi kan rita upp följande arvshierarki eller klasshierarki över klasserna.

                         ======
                         Figur:
                         ======
                   attribut: x, y
                   metoder:  konstruktor
                /            destruktor
               /             flytta, skriv, yta, rita
              /                              \
             /                                \ 
            /                                  \
           /                                    \
       =======                              ==========
       Cirkel:                              Rektangel:
       =======                              ==========
attribut: radie                      attribut: hoejd, bredd
metoder:  konstruktor                metoder:  konstruktor
          destruktor                           destruktor
          skriv, yta, rita                     skriv, yta, rita
                                            /                        
                                           /                        
                                          /
                                      ========
                                      Kvadrat:
                                      ========
                                   attribut: -
                                   metoder:  konstruktor
                                             destruktor
                                             skriv, yta, rita

 

Fakta om arvingar och deras egenskaper (attribut)

·   Man säger att Figur är en basklass (superklass).

·   Klassen Cirkel är en härledd klass (sub-klass) (derived).

·   De härledda klasserna Cirkel, Rektangel ärver alla attribut och metoder från klassen Figur. Dvs de härledda klasserna har automatiskt attributen x, y.
Klassen Cirkel har dessutom attributet radie och klassen Rektangel har dessutom attributen hoejd, bredd.

·   Klassen Kvadrat ärver x, y från Figur och dessutom hoejd, bredd från Rektangel.

·   Man säger att Kvadrat är direkt härledd från Rektangel och indirekt härledd från Figur. Den har inga egna extraattribut.

Arv - kod i C++

Vi rullar vidare med Figur-exemplet från föregående avsnitt....

           ****         
        *        *      
      *     =====  *     
     *    c i       *           //
     *//    r k     *          //====a==r==v=====
     *///     e l   *          \\================
     * ////      == *           \\
      * //////     *    
        */////// *      
           ****         ......     ....     .....    ....  

 

Exempel på ett huvudprogram

Vi börjar med att visa ett testhuvudprogram som har en figur figuren, en cirkel c, en rektangel r och en kvadrat fyrkant.

int main()
{
  Figur figuren;                        //(1)
  figuren.flytta( 0.5, 7.3 );
  figuren.rita();
  figuren.skriv(); cout << endl;
  cout << "Ytan är = " << figuren.yta()
       << endl;
 
  Cirkel c( 3.0, 4.0, 10.0 );           //(2)
  c.flytta( 1.0, 1.0 );
  c.rita();
  c.skriv(); cout << endl;
  cout << "Ytan är = " << c.yta()
       << endl;
 
  Rektangel r( 15.0, 4.0, 15.0, 8.0 );  //(3)
  r.flytta( 4.5, 1.1 );
  r.rita();
  r.skriv(); cout << endl;
  cout << "Ytan är = " << r.yta()
       << endl;
 
  Kvadrat fyrkant( 10.0, 10.0, 3.0 );   //(4)
  fyrkant.flytta( 0.03, -0.04 );
  fyrkant.rita();
  fyrkant.skriv();  cout << endl;
  cout << "Ytan är = " << fyrkant.yta()
       << endl;
 
  return 0;
}

För att se körningsexempel samtidigt, klicka på länken nedan, dvs arv0.txt.

Exempel

Kommentarer:

Objekt på olika platser i hierarkin skapas. För alla objekt utför vi samma operationer, nämligen: deklarera (skapa), flytta position i rummet, rita, skriva information och slutligen skriva ut figurens yta.

·   (1): Som vi ser i körningsexemplet är en figur egentligen inte användbar i sig själv. Den har inget utseende, ingen direkt yta. Metoderna yta och rita är i sig meningslösa. De finns där enbart för att ärvas vidare nedåt, dvs så vi är säkra på att alla subfigurer (cirklar, rektanglar,...) har en yta och en skepnad. Det enda "vettiga" superklassen har är attributvärden, dvs för positionen, (x, y). Klassen Figur finns egentligen bara till för att vara moder till de mer konkreta figurklasserna Cirkel och Rektangel.

Det är inte alltid superklasser har denna egenskap, men det händer ganska ofta.

·   (2): Här ser vi att metoden flytta kan användas av cirkelobjekt. En cirkel ritas. Vid utskriften avslöjas att en cirkel är en figur.

·   (3): Här ser vi att metoden flytta kan användas av rektangelobjekt. En rektangel ritas. Vid utskriften avslöjas att en rektangel är en figur.

·   (4):Här ser vi att metoden flytta kan användas av kvadratobjekt. En kvadrat ritas. Information om kvadraten skrivs ut (men man ser även att en kvadrat är en rektangel som är en figur).

Klassdefinitioner

Låt oss se hur klasserna hänger ihop. Vi visar alla klassdefinitioner nedan:

Klassen Figur

class Figur
{
protected:      // protected: enkelt och bra vid arv
  double x, y;
 
public:
  Figur();
  Figur( double inx, double iny );
  void flytta( double dx, double dy ); 
  void skriv();
  double yta();
  void rita();
};

Som du ser har vi bytt private mot protected. Med detta menas att attributen är skyddade, vilket i praktiken innebär att attributen kan användas av alla metoder i klassen Figur, samt i alla metoder i klasser som är härledda från Figur, dvs här Cirkel, Rektangel, Kvadrat.

En skyddad medlem (attribut eller metod) blir tillgänglig inom arvshierarkin, men inte utanför. T.ex. i huvudprogrammet eller i metoder till andra klasser, då fungerar det precis som vanlig private-deklaration gjort hittills i kursen.

Man kan ha private-deklarerade attribut vid arv också, men då kan attributen bara användas i metoder till superklassen (här Figur). Om man inne i metoder i de härledda klasserna vill komma åt attributen, så måste man använda metoder i superklassen för detta.

Klassen Cirkel: public Figur

class Cirkel : public Figur
{
protected:    
  double radie;
 
public:
  Cirkel();
  Cirkel( double inx, double iny, double inradie );
  void skriv();
  double yta();
  void rita();
};

Att arv sker över huvud taget märks på raden:

class Cirkel : public Figur

Det finns olika varianter på hur man kan ärva, men i TDB2 använder vi alltid public-deklarerade arv, även kallat synligt arv. I praktiken innebär det att "allt är som vanligt", dvs alla medlemmar i superklassen som är skyddade (här: attributen), kommer att vara skyddade även i subklassen och alla medlemmar i superklassen som är synliga (=publika, dvs här: metoderna) kommer att vara synliga även i subklassen.

Vi ser att metoden flytta ej deklareras i Cirkel-klassen, men ändå kan användas för cirklar (se körningsexemplet ovan) - den är ju ärvd från Figur-klassen.

Man får lägga till nya metoder i subklasserna, men vi har det ej i detta exempel. Då kan naturligtvis bara klasser nedåt i hierarkin utnyttja denna metod - aldrig superklassen!

Det är tillåtet, och dessutom mycket vanligt, att man har samma namn på en metod i en subklass som i en basklass. I så fall säger man att att den nya metoden döljer basklassens metod. I klassen Cirkel deklareras en ny metod med namnet skriv. Denna döljer skriv-metoden som deklarerats i Figur.

Funktionsöverlagring fungerar alltså ej mellan klasshierarkier. Om en metod (medlemsfunktion) är deklarerad i en sub-klass så döljs alla metoder med samma namn i basklassen (ober. av hur parameterlistor ser ut).

Exempel:

  Figur figuren( 3.5, -3.3 );
  figuren.skriv();      // skriv-metod i Figur-klassen anropas
 
  Cirkel c( 3.0, 4.0, 10.0 );           
  c.skriv();            // skriv-metod i Cirkel-klassen anropas

 

Klassen Rektangel: public Figur

class Rektangel : public Figur
{
protected:
  double hoejd, bredd;
 
public:
  Rektangel();
  Rektangel( double inx, double iny, 
             double inhoejd, double inbredd );
  void skriv();
  double yta();
  void rita();
};

Resonemanget för Cirkel gäller även för klassen Rektangel. Se ovan.

Klassen Kvadrat: public Rektangel

class Kvadrat : public Rektangel
{
protected:
public:
  Kvadrat();
  Kvadrat( double inx, double iny, double inbredd );
  double yta();
  void rita();
  void skriv();
};

Raden

 class Kvadrat : public Rektangel 

anger att Kvadrat är arvinge till klassen Rektangel (som vi vet i sin tur är arvinge till Figur). Metoden skriv döljer de ovanliggande metoderna med samma namn.

Kvadrat fyrkant( 10.0, 10.0, 3.0 );  
fyrkant.skriv();  // skriv-metod i Kvadrat-klassen anropas

Arv - hur arbetar man?

Vi fortsätter med Figur-exemplet från föregående avsnitt....

      ______________
     |/    r e k    |
     |//  ====  t a |           //
     |///  n g  ====|          //====a==r==v=====
     |/////   e  l  |          \\================
     L______________J  .....    \\     .....   .....

 

Konstruktor & destruktor

Det finns några funktioner som ej ärvs - nämligen konstruktorer och destruktorn.

Konstruktor

Objekt som hör till subklasser (t.ex. Cirkel) initieras med hjälp av konstruktorer (som vanligt). När en sådan konstruktor används, så måste konstruktorn för superklassen (dvs Figur) användas för att sätta värden på de härledda attributen (här: x, y) i subklassen.

Exempel: standardkonstruktorn och överlagrade konstruktorn till klassen Cirkel:

Cirkel:: Cirkel()
  : Figur(), radie(0.0)
{}
 
Cirkel:: Cirkel( double inx, double iny, 
                       double inradie )
  : Figur( inx, iny ), radie(inradie)
{}

Ovan har vi två konstruktorer, var och en med en initieringslista som börjar med att basklassens standardkonstruktor, Figur(), respektive överlagrade konstruktor, Figur( inx, iny ) anropas, sedan initieras de övriga attributen.

Det är viktigt att tala om för kompilatorn vilken konstruktor i basklassen som skall anropas; den har ju fler att välja på (standardkonstruktor, överlagrade konstruktorer). I initieringslistan gäller att C++ automatiskt anropar basklassens konstruktor innan subklassens attribut initieras, även om man skriver initieringen i annan ordning.

I subklassens standardkonstruktor, Cirkel() behöver man ej explicit anropa basklassens standardkonstruktor Figur(), det görs alltid automatiskt - förutsatt att det finns en sådan konstruktor. Dock väljer vi att göra det explicit i alla fall, för att få mer lättförståelig och konsekvent kod.

(Vi nämnde i aggregat-lektionen att man alltid bör skriva egna standardkonstruktorer, just för att de anropas automatiskt ibland, t.ex. vid aggregat och arv).

Destruktor

Destruktorer behöver ej anropas explicit. Detta görs automatiskt, ty det finns ju bara en destruktor per klass att välja på. Allt sker i omvänd ordning jämfört med konstruktorerna. För destruktorer gäller att först destrueras den mest härledda klassen, sedan den näst mest, ...etc. Slutligen destrueras basklassdelen.

Destruktorer behövs bara då motsvarande konstruktor eller någon av klassmetoderna har allokerat minnesutrymme dynamiskt med new. Destruktorn frigör utrymme som ej behövs då levnadstiden för objekt tar slut. Detta görs med delete.

Metodimplementationer

Nedan visar vi Cirkel-klassens övriga metoder:

Metoden yta

double Cirkel:: yta()
{
  return 2*pi*radie;
}

Metoden yta är exempel på en metod som är olika i de olika subklasserna, beroende på vilken geometrisk figur det gäller. Den döljer basklassens metod med samma namn.

Metoden rita

void Cirkel:: rita()
{
  cout << "\nEn cirkel har denna form: " << endl;
  cout      <<  "           ****         \n" 
            <<  "        *        *      \n" 
            <<  "      *     =====  *    \n" 
            <<  "     *    c i       *   \n" 
            <<  "     *//    r k     *   \n" 
            <<  "     *///     e l   *   \n" 
            <<  "     * ////      == *   \n" 
            <<  "      * //////     *    \n" 
            <<  "        */////// *      \n" 
            <<  "           ****         \n";
}

Metoden "ritar" en cirkels utseende. Den döljer basklassens metod med samma namn.

Metoden skriv

void Cirkel:: skriv()
{
  // Anropa först den dolda metoden 
  // "skriv" i Figur-klassen, låt den
  // sköta utskrift av x, y.
  // Skriv sedan ut info om radien.
 
  Figur:: skriv();
  cout << "är en cirkel med radie = " << radie;
}

Om man vill kan man skriva ut alla attribut direkt, t.ex. skulle följande fungera lika bra:

void Cirkel:: skriv()
{
  cout << "Figur med position ( " 
       << x << ", " << y << " )" << endl;
  cout << "är en cirkel med radie = " << radie; } 

men vanligast är att man anropar basklassens utskriftsmetod explicit inifrån subklassens utskriftsmetod. För att kompilatorn skall fatta vems utskriftsmetod som anropas så anger man dem med klasstillhörighetssymbolen, ::, dvs Figur:: skriv();. Detta är alltså ett sätt att komma åt gömda medlemsfunktioner.

Exempel
Nedan finns hela koden för arvsexemplet + tillhörande körningsexempel.

Tilldelning av objekt - typomvandling

Eftersom en härledd klass (t.ex. Cirkel) alltid innehåller datamedlemmar (attribut, metoder) från basklassen (dvs Figur) som ett subobjekt, så kan man alltid göra typomvanling från härledda klassen till basklassen.

figuren:               pannkaka:
-----------            -----------
x:     10.0            x:      3.0
-----------            -----------
y:     10.0            y:      4.0
-----------            -----------
                       radie: 1.25 
                       -----------
 
             <<==== OKAY
             ====>> FEL!

Härledda klassens datamedlemmar "skalas av" och basklassens blir kvar.

Vanliga objekt

Tilldelning av objekt tillhörande klasser i arvshierarki sker enligt särskilda regler.

  Figur figuren( 10.0, 10.0 );
  Cirkel pannkaka( 3.0, 4.0, 1.25 ); 
 
  figuren = pannkaka; // Helt OK tilldelning,
                    // men radie kommer att "skalas av",
                    // ty Figur-objekt har ej radie-fältet.
 
  figuren.skriv();

Programmet ger utskriften:

Figur med position ( 3, 4 )

Att tilldela tvärtom i hierarkin går inte:

  Figur annan( 8.5, 4.5 );
  pannkaka = annan; // Ger kompileringsfel! Ej tillåtet.

 

Pekare till objekt

Samma typkompatibilitet som ovan gäller för pekare. Man kan tilldela bara i ena arvsriktningen. Exempel:

  figur_pek = cirkel_pek;  // OKAY!
  figur_pek->skriv();
 
  
  Figur *annanFigur_pek = new Figur( 3.3, 3.4 );
  cirkel_pek = annanFigur_pek;     // FEL!
  cirkel_pek->skriv();

En pekare till en härledd klass kan alltså automatiskt omvandlas till en pekare till basklassen. I praktiken innebär det att figur_pek får peka på objekt som hör till arvingarna till superklassen, dvs här Cirkel, Rektangel, Kvadrat.

Exempel
Nedan finns koden med huvudprogram som testar tilldelning kors och tvärs i arvshierarkin.

Arv - polymorfi

Dynamisk bindning fås enbart då man arbetar med pekare av typen "pekare till basklassen". I C++ gäller att pekare till basklassobjekt kan fås att peka på objekt som tillhör någon av de härledda klasserna. Vid kompileringen vet man ej vilkan slags objekt pekaren skall peka på, utan det bestäms under körningen. En stor flexibilitet hos språket!

Vi fortsätter med gamla Figur-exemplet från föregående avsnitt....

      ________
     |/  k v  |           //
     |//  a  =|          //====a==r==v=====
     |/// d r |          \\================
     |///  a t|           \\    
     L________J  .....          .....       .....

 

Polymorfi och dynamisk bindning

Statisk bindning

Som du vet har vi metoder med samma namn, fast på olika nivåer i hierarkin. T.ex. skriv, yta, rita. Exempel ur main:

Cirkel c( 3.0, 4.0, 10.0 );           
c.rita();
Rektangel r( 15.0, 4.0, 15.0, 8.0 );  
r.rita();

I kodsnutten ovan sker s.k. statisk bindning eller tidig bindning. I deklarationen framgår att variabeln c har "datatypen" Cirkel. Eftersom typen är känd vid kompileringen, så vet kompilatorn direkt att den kan välja cirkelklassens ritametod, dvs void Cirkel:: rita().

Dynamisk bindning

För att visa dynamisk bidning, eller sen bindning, så introducerar vi en pekare, figur_pek:

Figur *figur_pek;

Typen på figur_pek är "pekare till Figur-objekt". Vi vet redan att typomvandling från en härledd klass till basklass är möjligt, dvs en pekare till en härledd klass kan automatiskt omvandlas till en pekare till basklassen. Pekaren figur_pek får alltså peka på objekt som hör till Cirkel, Rektangel, Kvadrat.

Exempel:

Figur *figur_pek;
Cirkel c( 3.0, 4.0, 10.0 );           
 
figur_pek = &c;
figur_pek->rita();

ger dock vid körning följande:

En Figur har ingen speciell form!

Inte så bra! Dvs datorn fattade i alla fall inte att det var ett cirkelobjekt som pekades ut. Kompilatorn tror att Figur-pekaren bara kan peka på Figur-objekt.

För att få den dynamik vi eftersträvar så måste vi deklarera metoden rita som virtuell. Detta görs genom att skriva ordet virtual framför returdatatypen i funktionsdeklarationen i basklassen. Man behöver ej upprepa ordet virtual i alla subklassers metoddeklarationer.

class Figur
{
protected:      
  double x, y;
 
public:
  Figur();
  Figur( double inx, double iny );
  void flytta( double dx, double dy );
  virtual void skriv();   // dynamisk bindning
  virtual double yta();   // dynamisk bindning
  virtual void rita();    // dynamisk bindning
};

Nu får vi detta körningsresultat, vilket vi ville ha:

En cirkel har denna form: 
           ****         
        *        *      
      *     =====  *    
     *    c i       *   
     *//    r k     *   
     *///     e l   *   
     * ////      == *   
      * //////     *    
        */////// *      
           ****

Dynamisk bindning innebär att när programmet körs, så undersöks vilket slags objekt som figur_pek pekar på just då och motsvarande metod anropas. Glöm ej att virtual-deklarera metoder av denna typ!

Lite större program

Ett annat huvudprogram, som visar flexibiliteten med pekare och arv är detta:

int main()
{
  Figur *figur_pek;
  int igen; 
 
  do
    {
      int typ;
      // Läs in så att man är säker på att talet är
      // 1, 2 eller 3!
      do
           {
             cout << "Vad vill du skapa? " << endl
                  << "1=cirkel, 2=rektangel, 3=kvadrat"  
                  << endl;
             cin >> typ;
           } while ( typ <= 0 || typ >= 4 );
      
      if ( typ == 1 )
           {
             // Skapa cirkel
             figur_pek = new Cirkel(1.0, 2.0, 1.0); 
           }
      else if ( typ == 2 )
           {
             // Skapa rektangel
             figur_pek = new Rektangel(1.0, 2.0, 5.0, 10.0); 
           }
      else if ( typ == 3 )
           {
             // Skapa kvadrat
             figur_pek = new Kvadrat(1.0, 2.0, 5.0); 
           }
      
      // Skriv ut info om och rita formen för
      // det som figur_pek pekar på:
      
      figur_pek->skriv(); cout << endl;
      figur_pek->rita();
      cout << "Ytan hos figuren är " 
              << figur_pek->yta() << endl;
 
      cout << "Vill du leka mera? (0=nej)" << endl;
      cin >> igen;
    } while (igen != 0 );
 
  cout << "Tack och hej!" << endl;
 
  delete figur_pek;   // städa upp lite...
  return 0;
}

Hela koden och körningsexempel finns nedan.

Exempel
Program som illustrerar polymorfi och arv i C++.

Abstrakta klasser

Metoderna rita och yta i klassen Figur är egentligen ganska meningslösa. De ser ut så här:

double Figur:: yta()
{
  // Egentligen har en Figur ingen yta...
  return 0;       
}
 
void Figur:: rita()
{
  cout << "En Figur har ingen speciell form!" << endl;
}
 

vilket känns lite onödigt. Metoderna utför inget vettigt. Anledningen till att de finns över huvud taget är att vi vill vara säkra på att alla klasser som härleds ur klassen Figur har en rita-metod och en yta-metod.

Man slipper dessa onödiga implementationer av metoderna om man deklarerar dem som rent virtuella metoder med = 0 direkt till vänster om semikolonet i deklarationsraden:

class Figur
{
protected:      // protected: enkelt och bra vid arv
  double x, y;
 
public:
  Figur();
  Figur( double inx, double iny );
  void flytta( double dx, double dy );
  virtual void skriv();
  virtual double yta() = 0; // rent virtuell
  virtual void rita() = 0;  // rent virtuell
};

En rent virtuell metod är en slags markering att denna metod skall finnas på lägre nivåer. Superklassen får inte ha någon implementation av denna metod.

En klass som har minst en rent virtuell metod kallas abstrakt klass. Den används bara som en slags mall eller mönster och visar hur subklasser ser ut. Man får inte ens deklarera objekt hörande till klassen, men man kan deklarera pekare till härledda objekt på samma sätt som ovan. Se exempelkod + körningsexempel nedan.

Exempel
Program som illustrerar abstrakt klass i C++:

Arv - laboration

Arv

Man kan beskriva husdjurs relationer med arv:

·   Favoritdjur har attributen (egenskaperna) namn och ras.

·   Hund är ett favoritdjur som har lång/kort päls.

·   Katt är ett favoritdjur som är en inne/ute katt.

Relationer

·   En relation finns mellan favoritdjur (basklass) och hund/katt (subklass).

·   Basklassen (X) är stamfadern/modern subklassen är arvinge (Y).

·   Y är en/ett X som...

·   Y är arvinge till X.

·   Relationerna kan beskrivas i en klasshierarki (jmfr släktträd).

Arv i C++:

·   Datafält (attribut) och metoder kan ärvas enkelt från basklasser om de sätts till protected.

·   Protected är en nivå mellan private och public.

·   Arvingar kan direkt komma åt det som är satt till protected.

Objekt i en klasshierarki kan tilldelas varandra efter särskilda regler. Om hierarkin har tre nivåer, dvs X är objekt i basklassen, Y är arvinge generation 1 till basklassen och Z är arvinge generation 2 till basklassen, så gäller följande:

·   Tillåtna tilldelningar: X = Y; Y = Z; X = Z;

·   EJ tillåtna tilldelningar: Y = X; Z = Y; Z = Y;

·   Sammanfattningsvis: bara tilldelningar uppåt i hierarkin. Skälet är att datafält annars blir oinitierade.

Svar

Lösningsförslag till uppgifterna.

Uppgift 1

Se petdog.cpp som är ett program med klassen pet och klassen dog. Kopiera hem filen, kompilera och testkör. Komplettera sedan med att skapa en klass cat med lämpliga metoder:

const int INDOOR = 0;
const int OUTDOOR = 1;
string strdoor[2]={"indoor","outdoor"};
 
class cat : public pet
{
private:
  int inOutDoor; // INDOOR/OUTDOOR
 
public:
  cat();
  cat( string NewName, string NewBreed, int NewInOut );
  ~cat();
  void print();
  void sound();
};

Metoden print skriver ut info om katten och sound läter katten säga "MJAU". Komplettera main så att kattobjekt också finns med.

Uppgift 2

Om man använder pekare, så blir det fel om man inte tänker sig för...

·   Kopiera petvirtual.cpp, kompilera och testkör.

·   Vad händer om du tar bort virtual i deklarationen av basklassen? Kompilera om och testkör.

·   Ändra så destruktorn för pet också är virtuell, kompilera, testkör. Vad händer?
Du kan även testa petnonvirtual.cpp som jämförelse om du vill...


Lösningsförslag

1.      Uppgift 1: (startfil: petdog.cpp)
losningarv.cpp

Arv - sammanfattning

Liten sammanfattning om arv i C++.

Syftet

Arv är speciellt för de objektorienterade språken och ingår som en central del i dessa.

·   Arv innebär att klasser kan definieras i termer av redan existerande klasser.

·   Syftet med arv brukar vara tvåfaldigt:

o  Att redan skriven kod skall kunna återanvändas.

o  Att superklasser skall kunna definiera ett gemensamt beteende för subklasserna.

Mer detaljer om arv...

·   Olika klasser kan ha släktskap med varandra och bilda ett släktträd, en hierarki i olika nivåer. För vissa tillämpningar är detta naturligt.

·   Man kan återanvända gamla klasser och istället för att utvidga/förstöra dem, så skapar man subklasser istället, som ärver egenskaper + har sina egna nya.

·   Enklast om man har protected i klassdefinitionerna.

·   Arv är speciellt för de objektorienterade språken och ingår som en central del i dessa. Kunskaper om arv är bra både för att skapa egna klasser och för att förstå vad som händer när man använder färdigdefinierade klasser.

·   Metoder med samma namn som i basklassen döljer metoden i basklassen.

·   Konstruktorer och destruktorer ärvs inte.

·   Arv kan ske i flera led (subklasser kan ha subklasser).

·   Typomvandlingar kan ske från t ex härledd- till bastyp. Dessa går alltid bra, medan det är osäkrare åt andra hållet. I en konstruktor för en härledd klass kan man i initieringslistan explicit anropa en konstruktor för basklassen.

·   Med polymorfi och dynamisk bindning menas att en viss konstruktion kan exekveras på olika sätt, t.ex. överlagrade funktioner. Med statisk (tidig) bindning avses att vilken funktion som skall anropas avgörs vid kompilering. Med dynamisk (sen) bindning avses att vilken funktion som skall anropas avgörs vid exekvering. Detta implementeras med virtuella funktioner.

·   En ren virtuell funktion är en sorts markör för en funktion som måste definieras i en härledd klass. En klass med någon sådan ren virtuell funktion kallas abstrakt. Objekt kan inte skapas av abstrakta klasser. De tjänar som mönster för hur de olika subklasserna skall se ut och blir en beskrivning av vilka operationer som måste finnas.

·   Med arv och dynamisk bindning får man enkla och eleganta lösningar - t.ex. kan man lägga till nya objekt till koden senare, utan några större problem.